ARTICLE DETAIL

资讯详情

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

基于Vue3与SpringBoot的博客打赏系统全栈实现

基于Vue3与SpringBoot的博客打赏系统全栈实现 做个人博客的都知道写文章容易让读者愿意为内容付费才是真正考验。一开始我在文章底部贴了个收款码结果发现三个问题收了多少不知道、谁打赏的不知道、哪篇文章带来的收益更不知道。后来有朋友建议我直接做一个带支付闭环的博客打赏系统——从读者点击赞赏、选择金额、扫码支付到后台自动生成订单记录全链路自己掌控。这个项目用 Vue 3 Spring Boot 2.7 开发前后端分离既能当毕设项目应付答辩也能真实部署到自己的服务器上长期使用。这篇博文从业务设计、表结构、前端组件、后端接口到联调时踩过的坑和部署上线完整走一遍。1. 打赏系统的业务闭环与功能边界先想清楚再动手1.1 打赏不是放个收款码这么简单很多人以为打赏系统就是把收款码换成网页版的二维码但实际做下来差别很大。收款码方案没有订单概念付款方和收款方全凭信任账目一团乱麻。而一个正经的打赏系统核心是订单状态机驱动业务流转。读者看到一篇好文章点击赞赏按钮选择金额比如 6/16/66/166 或自定义填一句想说的话确认后系统生成一个待支付订单订单返回一个支付二维码读者扫码付款支付平台回调通知后端后端把订单状态从待支付翻转为已支付更新打赏时间前端轮询到订单已支付弹出感谢界面把寄语写进打赏记录列表。整套流程的核心就是围绕订单状态的迁移做文章。我最终设计的订单状态只有三个状态含义迁移路径0待支付创建订单后的初始状态1已支付回调验签成功后由 0 转为 12已关闭超时未支付或主动取消别小看这三个状态很多同学习惯用已支付/未支付两个布尔值一遇到超时关闭、退款、重复回调就不知道怎么处理了。状态机的好处是让每一步迁移都有明确的触发条件和业务规则后续加功能比如退款状态也只需要在枚举里加一个值而不是改一堆 if/else。1.2 功能边界不做登录不做结算聚焦支付闭环设计之初我就划清了边界。打赏系统不做用户登录注册——打赏本身就是临时的、轻量的行为如果为了赏一块钱先注册账号转化率直接归零。订单表里不强制关联用户 ID只记录打赏人自己填写的昵称和寄语匿名打赏完全支持。不做提现和结算——平台级的分账逻辑超出个人博客的需求钱款直接进入博主自己的微信/支付宝账户系统只负责记录订单数据方便对账和展示。这样划清边界之后整个系统的职责非常清晰前端提供一个体面的打赏入口和记录展示后端负责订单全生命周期管理和支付回调再加上一个简单的后台订单管理页一个完整的毕设项目就立住了。1.3 核心数据表设计订单表承载一切订单表是这套系统的地基设计得好前后端都省事。我的reward_order表字段如下金额统一用分存储CREATE TABLE reward_order ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 业务订单号, article_id bigint(20) DEFAULT NULL COMMENT 文章ID0表示赞赏作者, article_title varchar(255) DEFAULT NULL COMMENT 文章标题快照, amount bigint(20) NOT NULL COMMENT 打赏金额单位分, pay_type tinyint(4) NOT NULL COMMENT 支付方式1微信 2支付宝, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已关闭, third_trade_no varchar(64) DEFAULT NULL COMMENT 第三方交易号, qr_code_url varchar(512) DEFAULT NULL COMMENT 支付二维码内容, reward_nickname varchar(64) DEFAULT NULL COMMENT 打赏人昵称, reward_msg varchar(255) DEFAULT NULL COMMENT 打赏寄语, create_time datetime DEFAULT NULL COMMENT 下单时间, pay_time datetime DEFAULT NULL COMMENT 支付时间, PRIMARY KEY (id), KEY idx_order_no (order_no), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT打赏订单表;两个容易忽略的设计点article_title 存的是快照而不是通过 article_id 关联查询。因为文章未来可能被删除或改名但打赏记录是历史事实必须保留当时的上下文。列表页展示打赏记录时直接读这个字段就够用了。金额使用 bigint 存分前端展示时除以 100 转换。支付相关的金额计算坚决不用float/double否则浮点误差会在对账时给你上一课。技术选型上后端我用 Spring Boot 2.7.x MyBatis-Plus MySQL 8.0JDK 用的 8。现在 Spring Boot 3.x 已经普及但注意 3.x 强制要求 JDK 17很多学校实验室和老服务器还在 JDK 8如果你不希望部署环节被版本问题卡住老老实实用 2.7.x 是性价比最高的选择。前端用 Vue 3 Vite Element Plus Axios二维码生成用qrcode库组件库的弹窗、按钮、输入框直接拿来用开发效率高。2. 前端 Vue 实现打赏弹窗、二维码渲染与轮询逻辑2.1 打赏入口与弹窗组件结构我在博客文章页底部放了一个赞赏作者按钮点击后弹出打赏弹窗。弹窗组件我命名为RewardModal.vue用 Vue 3 的Teleport挂到 body 节点下避免被父级组件的overflow: hidden等样式影响。组件内部是典型的三段式结构金额选择区 → 二维码展示区 → 支付结果区。用currentStep控制切换三段之间通过状态变量连接。第一步是金额选择。预设金额我做了几个常用档位6、16、66、166再加一个自定义输入框。这里有个交互细节值得说明预设金额是点击即选而自定义金额需要配合确定按钮避免用户还没来得及输入组件就触发了下一步。自定义金额的校验必须做两层第一层用inputmodedecimal唤起数字键盘第二层在确定时校验正则const validateAmount (val) { if (!val) return false const num Number(val) if (isNaN(num) || num 0.01 || num 10000) return false if (!/^\d(\.\d{1,2})?$/.test(val)) return false return true }2.2 创建订单与二维码渲染用户确认金额和寄语后前端调用POST /api/reward/create接口参数包括文章 ID、金额、支付方式、昵称、寄语。后端返回两个关键字段orderNo和qrCodeUrl。qrCodeUrl是支付平台生成的支付链接或测试模式下的模拟支付链接前端拿到后在 canvas 上渲染成二维码。二维码渲染我用的是qrcode库它支持toCanvas方法比生成 base64 图片再塞进 img 标签更干净import QRCode from qrcode const renderQr async (content) { await nextTick() if (qrCanvas.value) { QRCode.toCanvas(qrCanvas.value, content, { width: 200, margin: 2 }) } }生成二维码之前还需要一个loading状态因为从点击金额到二维码渲染成功之间有网络耗时没有任何反馈的话用户很可能会重复点击导致后端创建出多个订单。2.3 支付状态轮询五秒为王二维码展示出来之后系统需要知道用户什么时候扫完付了款。最简单可靠的方式是前端轮询订单查询接口。我最初考虑过 WebSocket 推送但掂量了一下打赏本身是高延迟、低频次的场景用户扫码后可能去微信里输入密码、等指纹识别这个时间可能长达几十秒推送通道在复杂网络环境下反而容易断开轮询则简单得多服务端重启也不怕丢消息。轮询的间隔我选了 5 秒一次理由有两个太频繁会给后端造成无意义压力太慢则破坏体验。用户付完款后最多 5 秒就能看到成功界面心理上完全可接受。let pollTimer null const startPolling () { stopPolling() pollTimer setInterval(async () { const res await queryRewardOrder(orderNo.value) if (res.data.status 1) { stopPolling() currentStep.value success } }, 5000) } const stopPolling () { if (pollTimer) { clearInterval(pollTimer) pollTimer null } } onBeforeUnmount(() { stopPolling() })轮询最容易犯的错误就是忘记在组件卸载时清理定时器。用户等得不耐烦直接关了弹窗但定时器还活着每隔几秒就发一次请求直到页面跳转才被销毁。我做了一个stopPolling方法弹窗关闭和组件卸载两处都要调用。2.4 支付成功后的交互与打赏榜单轮询到支付成功后前端切到成功界面。我在成功界面放了两样东西一句感谢支持和一个可选的寄语输入框。用户已经付了钱这时候再让他填一句想说的话意愿是最高的转化率远高于支付前填写。填完寄语调用POST /api/reward/comment更新订单的reward_nickname和reward_msg字段。文章页底部还有一个最新打赏列表展示最近 10 条已支付记录内容包含打赏人昵称、金额和时间。这里通过GET /api/reward/list?articleIdxx接口查询后端只返回status 1的订单打赏人才会出现在榜单上。这个榜单虽然简单但它把零散的支付行为变成了公开的社交认同读者之间互相看到打赏的积极性会被刺激起来。3. 后端 Spring Boot订单接口、回调验签与幂等落库3.1 创建订单接口与订单号生成后端的RewardController有三个核心接口创建订单、查询订单、支付回调。外加一个打赏列表接口。创建订单的流程是这样的Service public class RewardService { Transactional public RewardOrderVO createOrder(RewardCreateDTO dto) { // 1. 服务端再次校验金额合法性防止前端绕过 if (dto.getAmount() null || dto.getAmount() 1 || dto.getAmount() 1000000) { throw new BizException(打赏金额不合法); } // 2. 生成业务订单号 String orderNo generateOrderNo(); // 3. 组装订单记录 RewardOrder order new RewardOrder(); order.setOrderNo(orderNo); order.setArticleId(dto.getArticleId()); order.setArticleTitle(dto.getArticleTitle()); order.setAmount(dto.getAmount()); order.setPayType(dto.getPayType()); order.setStatus(0); order.setCreateTime(new Date()); rewardOrderMapper.insert(order); // 4. 调用支付平台生成支付链接或测试模式模拟链接 String qrCodeUrl payService.getPayQrCode(orderNo, dto.getAmount()); order.setQrCodeUrl(qrCodeUrl); return convertToVO(order); } }订单号我用了时间戳 随机数 支付方式的组合长度控制在 30 位以内。不用数据库自增 ID 当订单号是因为订单号会出现在二维码参数和回调通知里暴露表结构的信息越多被恶意构造的风险越大。订单号生成规则属于业务规则要写在服务端不能用 UUID 那种纯随机的否则出问题排查的时候对账会让人崩溃。时间戳前缀让他一眼能看出下单时间排查问题非常方便。3.2 支付对接的两种路径真实商户号与测试模式这里必须区分两种场景因为很多个人开发者根本没有商户号。场景一有商户号个体工商户或企业资质。以微信支付 Native 为例后端调用统一下单接口传入订单号、金额分、回调地址notifyUrl支付平台返回code_url这个链接就是前端要渲染成二维码的内容。支付宝对应用的是当面付的qr_code。这一套官方文档写得很全核心就是构造签名、请求接口、拿二维码。场景二个人博客没有商户号。这是大多数开发者的真实处境。我在项目里增加了测试模式当pay.enabledfalse时创建订单不调用真实支付平台而是直接返回一个模拟的支付链接比如http://server/api/reward/mock-pay?orderNoxxx前端二维码照样渲染。同时后端留一个POST /api/reward/mock/notify调试接口用来模拟支付成功回调。这样既能在没有商户号的情况下把整条业务链路跑通、录演示视频给老师看也能保证代码结构里留好了真实支付对接的空位。要强调的是测试模式只是开发调试和演示用的正式上线必须关闭。真实支付环境下的回调通知包含金额、商户号、交易号等字段需要严格验签不能盲目信任来源。3.3 回调验签与幂等更新安全与并发都要管支付回调是整个系统最关键的环节也是最容易被攻击的地方。一个伪造的回调请求可能让订单在没收到钱的情况下被标记为已支付所以在回调入口必须做验签。微信支付官方提供了WXPayUtil.isSignatureValid(Map, apiKey)方法内部逻辑是把所有参数按字典序排序拼成keyvaluekey2value2...最后拼接商户密钥做 MD5 或 HMAC-SHA256再与回调里的sign字段比对。签名一致才认为消息来自微信支付不一致直接拒绝。验签通过之后还有一个幂等更新的问题。支付平台在极端情况下会重复发送回调网络超时重试、平台内部重试机制等同一个订单可能收到两三次通知。如果更新逻辑是先查订单状态等于 0 就更新为 1在并发场景下依然可能重复入账。正确做法是把状态条件直接写进 UPDATE 语句int updated rewardOrderMapper.updateStatusByOrderNo( orderNo, 1, thirdTradeNo, payTime, RewardOrderStatus.CREATED.getCode() // 只允许从待支付改为已支付 ); if (updated 1) { // 本次回调真正完成了订单可以触发后续动作通知博主等 } else { // 订单不存在或状态已变更直接返回成功避免平台无限重试 }对应的 SQL 是UPDATE reward_order SET status #{newStatus}, third_trade_no #{thirdTradeNo}, pay_time #{payTime} WHERE order_no #{orderNo} AND status #{oldStatus}MyBatis-Plus 的update方法配合wrapper.eq(status, 0)也能实现同样的效果。这种条件更新从数据库层面保证了并发安全比 xmx 先查再改要可靠得多。我当时在测试里模拟了 20 个并发回调线程同时打同一个订单号最终状态只更新了一次记录结果完全一致这就是幂等设计的价值。3.4 后台管理订单列表与状态筛选打赏系统不能只有前端展示管理后台是必需的。我在项目中加了一个轻量管理页用 Vue 单独做了一个路由/admin/orders后端提供GET /api/reward/page接口支持按状态筛选待支付/已支付/已关闭、按订单号模糊搜索、按时间排序。表格展示字段包括订单号、文章标题、金额后端转成元、支付方式、状态、下单时间、支付时间。这块内容对毕设答辩特别重要很多同学做的系统只有前台没有后台答辩时评委一问你怎么管理订单就哑火了。哪怕只是一个列表页加筛选功能也能完整展示你对业务的理解。4. 前后端联调高频翻车点与修复记录4.1 跨域配置CORS 的坑和正确姿势前端跑在 5173Vite 默认端口后端跑在 8080跨域问题必然出现。Spring Boot 解决跨域的标准做法是配置 CORS但很多人在allowCredentials上翻车——如果要允许携带 CookieallowedOrigin就不能设为*否则请求直接失败。正确做法是用allowedOriginPatternsConfiguration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .maxAge(3600); } }前端如果用 Vite 开发也可以利用它的 proxy 配置把/api代理到后端开发环境完全不涉及跨域// vite.config.js export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })建议两种方案都掌握因为上线后走 Nginx 反向代理生产环境依然不跨域跨域配置主要用于开发和临时联调场景。4.2 LocalDateTime 序列化与长整型精度丢失联调时还遇到过两个隐蔽问题都很典型。第一个是时间格式化。MySQL 8.0 的 datetime 对应 Java 的LocalDateTime但 Jackson 默认序列化的格式是一长串数组前端拿到一脸懵。解决办法是统一配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8注意spring.jackson.date-format只对java.util.Date生效对LocalDateTime不生效。处理LocalDateTime要么在字段上加JsonFormat(pattern yyyy-MM-dd HH:mm:ss)要么全局注册JavaTimeModule。我更推荐全局配置避免每个字段都写注解。第二个是Long 精度丢失。数据库主键id是 bigint如果走默认 Jackson 序列化前端用Number接收时超过 2^5316 位就会丢失精度导致后续操作拿到的 ID 不对。我当时查了半小时最后发现前端收到的订单 ID 最后几位变成了 0才发现是精度问题。最简单的方案是全局配置 Long 转 String 输出Configuration public class JacksonConfig { Bean public Jackson2ObjectMapperBuilderCustomizer longToStringCustomizer() { return builder - { builder.serializerByType(Long.class, ToStringSerializer.instance); builder.serializerByType(Long.TYPE, ToStringSerializer.instance); }; } }或者干脆在 VO 中把订单号、主键都声明为String类型。顺序号和主键在前端本来就不需要参与算术运算当字符串用反而是最省心的。4.3 回调地址必须公网可达本地联调怎么破支付平台的回调通知是服务器发起的请求目标地址必须是公网能访问的。本地联调时后端跑在localhost:8080支付平台根本访问不到这是所有对接支付开发者的第一道坎。我当时的做法分两步正常开发阶段用测试模式模拟回调不用碰真实的网络链路等到需要真实支付联调时把后端代码打包部署到一台测试服务器上通过公网 IP 访问回调地址也配置成公网地址。对于毕设项目线上部署联调一次就够了别在本地死磕回调问题。前端轮询的/api/reward/query接口不需要公网可达因为那是用户浏览器发起的请求天然走公网。所以联调时可以这样组合后端部署在公网服务器前端本地跑前端代理指向服务器后端问题就都解决了。4.4 二维码渲染的细节大小、留白与清晰度二维码看着简单实际渲染时也出过问题。最开始我把 canvas 放在一个固定宽高的容器里但qrcode库生成的二维码自带白边显示出来非常小用户扫半天扫不上。后来我固定{ width: 200, margin: 2 }同时给 canvas 外面包了一层白色圆角容器避免浅色背景下二维码边缘识别不出来。还有一点二维码内容里如果包含中文字符某些版本的qrcode库会报错。支付链接通常是纯 URL一般不会踩中但如果测试模式里带上了中文昵称参数就要用encodeURIComponent编码后再放进链接里或者直接避免在二维码链接中拼接中文。5. 部署上线与后面还能怎么玩5.1 Nginx 托管前端 jar 包跑后端开发环境跑通后上线部署的思路是前后端分离的标准姿势前端构建成静态文件交给 Nginx后端打成 jar 包用 systemd 或 Docker 守护。前端构建流程非常简单npm run build构建产物在dist目录把它整个上传到服务器的/var/www/reward-blog目录Nginx 配置里指过去。有个关键配置是前端路由的 history 模式需要try_files兜底否则用户直接访问/article/1刷新页面时Nginx 找不到对应的物理文件会返回 404location / { root /var/www/reward-blog; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }后端构建启动mvn clean package -DskipTests nohup java -jar target/reward-blog-1.0.0.jar --server.port8080 app.log 21 生产环境建议用 systemd 管理 Java 进程日志、重启、开机自启都规范。如果服务器上装了宝塔面板用 Docker 部署 Spring Boot 也很常见——把 jar 包打进一个基础镜像挂载配置文件和数据卷维护起来更省心但毕设项目其实不需要为了上 Docker而上 Docker直接把 jar 扔在服务器上跑也完全合理。5.2 HTTPS 是支付回调的硬性要求微信支付和支付宝的回调地址都要求必须是 HTTPS 协议正常生产环境所以如果真实接入支付域名和 SSL 证书是逃不掉的。我推荐用宝塔面板一键申请 Lets Encrypt 免费证书或者去云厂商那申请免费的 DV 证书配置到 Nginx 上几分钟搞定。对于纯测试模式演示HTTP 也能跑但正式面向用户的博客HTTPS 本来也是标配。5.3 后续扩展方向别止步于毕设交差项目交付之后如果你想继续把它打磨成一个真正能用的个人博客组件有几个方向性价比很高打赏榜单与排行榜按文章维度聚合打赏金额做一个本周最受欢迎文章模块数据直接从reward_order表按article_idgroup by 查出来几张图表就能展示。新打赏通知订单支付成功时除了更新状态还可以触发邮件通知或 Webhook让博主第一时间知道有人打赏了。这个动作放在回调幂等更新的updated 1分支里正好是触发点。SE 实时提示前端页面用 Server-Sent Events 订阅订单变化支付成功后文章页可以弹一个轻量气泡通知比轮询更有仪式感代码也不复杂。打赏趋势分析管理后台引入 ECharts按天统计收入曲线按小时统计打赏高峰时段用于判断读者的活跃区间对内容运营有点参考价值。从个人实战体会来说做这类带着支付业务的项目最忌讳一开始就想着上 Redis、上 MQ、上微服务。支付订单的状态机、回调验签、幂等更新这些基本功才是核心先把一条支付链路端到端跑通再来谈架构和优化。这个打赏系统我从设计表结构到部署上线前后大概花了一周踩得最深的坑反而是 Long 精度丢失和 LocalDateTime 格式化这种看上去无关痛痒的小问题。如果你也在做类似的博客系统或毕设项目希望这些记录能帮你省下几个晚上的排查时间。
返回列表