☰
Spring Boot+Vue视频社区交易平台:从数据库到高并发实战
2026/10/27 12:38:22 网站建设 项目流程

简介:这是一套基于Spring Boot与Vue.js构建的微信小程序全栈项目,覆盖视频点播、直播、社区交流、评论、抢购、拍卖与短视频等典型业务模块,适合具备Java后端与Vue前端基础、希望快速上手小程序音视频社交类开发的开发者、学生或团队参考。压缩包共收录2000个文件,其中以621个Java源文件、534个JavaScript脚本、415个XML配置、289个JSON数据及86个Vue组件为主,另含SQL初始化脚本、YML环境配置、Maven打包命令、部署及证书文件等,整体仅8.49MB,文件类型丰富但目录结构清晰,便于按业务模块逐层拆解。目前已有668人学习下载。包内包含完整的前后端工程代码与运行配置,覆盖用户登录鉴权、视频点播与直播、社区评论、限时抢购及拍卖出价等功能链路,可帮助开发者理解小程序端与Spring Boot接口的交互方式,以及Vue项目与后端服务的协作机制;直接导入开发工具即可二次开发,适合用作毕业设计或商业原型基础。

1. 从标题看系统边界:这不是“小程序 + 后台”,而是一个视频社区交易平台

把这个标题当成一个普通商城项目来启动,第一周就会被直播和抢购两块按在地上摩擦。视频点播、直播、社区交流、评论、抢购、拍卖、短视频,听起来是七个功能点,实际是四条完全不同的技术线:内容生产与转码、实时音视频链路、社区 Feed 互动、高并发交易。它们会被装进同一个 Spring Boot 后端 + Vue 管理后台的工程里——这就是大多数团队真正卡住的地方。

这个项目方向适合三类人:做毕业设计想凑齐完整业务链的学生;一个人接私活、要快速交付全栈方案的全栈工程师;小团队想搭一个带内容属性和交易属性的小程序站点。读完你至少能把三件事想清楚:表怎么建才不会返工、视频和直播的链路怎么取舍、抢购和拍卖的并发问题怎么用最小成本解决。我不打算给你一份面面俱到的源码导读,而是顺着业务把方案讲透,让你能照着落地,也知道哪些地方必须提前给自己留后路。

2. Spring Boot + Vue 的整体架构:三个端、一条数据主线,先把工程拆对

2.1 “Vue”在小程序项目里到底扮演什么角色——管理后台,不是小程序本体

这里必须先破除一个误解:微信小程序的前端是 WXML/WXSS + JS 那套体系,就算用 uni-app 这类框架,也是把 Vue 语法编译成小程序代码。标题里的 Vue,在日常签合同和排期时,指的是管理后台。小程序端面向 C 端用户,负责视频点播、直播观看、社区发帖、评论互动、抢购拍卖下单;Vue 管理后台面向运营,负责上传视频、管理直播、审核社区内容、配置抢购和拍卖场次、看订单数据。

两端职责差很远,落地方式也不同。小程序端用原生开发还是 uni-app,取决于团队里有没有熟悉 Vue 的人——用 uni-app 可以让前端只用一套 Vue 语法,同时输出微信小程序和 H5,代价是遇到平台差异时得翻文档。Vue 管理后台则简单得多:Vue 3 + Vite + Element Plus 是现在的主流,Vue 2 的老工程配合 Element UI 也能做,只是组合式 API 和路由守卫写法会有差异。Spring Boot 后端自 2.7 升到 3.x 后,javax 命名空间换成了 jakarta,如果你下载的源码工程比较旧,升级时要把所有 import 一起换掉,这不是改一个配置就能跑起来的。

后端工程内部,我一般会按业务模块垂直切目录,而不是按 controller/service/mapper 横向铺开。这样做的原因是:等你真的扛不住流量要拆服务时,只需要把某一个垂直包整体挪出去,业务代码几乎不用动。拆出来的目录长这样:

src/main/java/com/example/vod/ ├── controller/ # 小程序端和管理端的接口入口,按 module 再分一层 ├── service/ │ ├── point/ # 点播与短视频 │ ├── live/ # 直播、回放、直播间互动 │ ├── social/ # 社区帖子、评论、点赞、关注 │ └── trade/ # 抢购、拍卖、订单、支付回调 ├── mapper/ # MyBatis 的 Mapper 接口与 XML ├── entity/ # 数据库实体 └── config/ # Redis、线程池、拦截器、统一异常处理

controller 层也要按端拆分:给小程序用的接口和给管理后台用的接口,路径前缀分开,比如/app/**和/admin/**。这不是为了好看,而是方便挂不同的拦截器——管理后台要校验管理员权限,小程序接口要校验用户登录态,混在一起写拦截规则会非常痛苦。如果你还在用 Spring Boot 2.3.x 这种老版本,config 里的拦截器注册方式基本一致,主要差异在依赖版本和部分 API 的包名,遇到编译报错优先查 spring boot 版本兼容表。

Vue 管理后台和 Spring Boot 后端是分开启动的,开发期跨域问题靠 Vite 代理解决。我在 vite.config 里习惯这样配:

export default defineConfig({ server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })

这样前端请求/api/login会被转发到后端8080端口,浏览器里看不到跨域报错。生产环境如果两个服务分开部署,就用 Nginx 反向代理,把/api指到后端内网地址;如果图省事,也可以把 Vue 打包后放到 Spring Boot 的静态目录里,后面第 2.3 节细说。

2.2 一条数据主线:用户、内容、互动、交易

任何视频社区交易平台的底层逻辑都很一致:用户进来 → 内容被消费 → 互动产生粘性 → 交易变现。对应到表结构上,主表就四组:用户表、媒体内容表(点播视频、短视频、直播回放)、社区内容表(帖子、评论、点赞关系)、交易表(抢购订单、拍卖订单)。小程序端的所有页面,都只是这四条主线上的不同组装方式。

我把三个端的职责用一张表列在这里,方便你做技术选型时对照:

端技术选型核心职责部署形态
小程序端原生 / uni-app(Vue 语法)消费视频、参与社区、下单交易微信平台托管
Vue 管理端Vue 3 + Vite + Element Plus上传内容、审核帖子、配置活动Nginx 或打进 Spring Boot 静态目录
Spring Boot 后端Spring Boot + MyBatis + Redis + MySQL统一服务、库存控制、支付回调、内容分发独立 jar 或在 Docker 中运行

这里面有一个容易被忽略的合规点:微信小程序的登录和手机号获取,必须依赖微信官方接口。现在的获取手机号已经不是简单的wx.login就能搞定,需要企业主体小程序并配置付费短信验证,个人开发者基本走不通。所以你在规划 Spring Boot 的登录服务时,要预留phone_code和phone两个字段位,等资质办下来再接。这两步我在做接口设计时一定会先写在文档里,免得后面改表。

2.3 管理端与后端一体化部署:Vue 打包后放进 Spring Boot 静态目录

前后端分离是开发期的主流方案,但很多小项目上线时并不想多养一台 Nginx。常见做法是把 Vue 管理后台构建出的dist目录,直接复制到 Spring Boot 的src/main/resources/static下,然后mvn clean package打成单个 jar。这样一个进程既提供管理后台页面,又提供/api接口,部署成本最低,也天然绕开了跨域问题。

这种做法有两个前提要说清楚:第一,以后每次改前端,都要重新打一次后端包再发布,发布流程变重;第二,Spring Boot 的application.yml里如果有server.servlet.context-path,静态资源路径和接口路径都会跟着变,Vue 里的路由 base 也要配合调整,否则页面刷新就 404。我自己的经验是:个人项目或演示项目用一体化部署省事;超过两个人在维护同一套系统,就老老实实分开部署,前端归前端发,后端归后端发,避免互相踩坑。如果你用 Spring Boot Admin 做基础监控,记得给 actuator 端点加上账号认证,直接暴露公网等于把服务器信息送人。

3. 先设计数据库:视频、社区、交易三组表决定了后面所有接口的写法

3.1 视频与短视频:两张表而不是一张

很多新手会把点播视频和短视频塞进同一张表,用type字段区分,省事。但实际业务里,点播视频通常有导演、分类、时长、清晰度、版权授权信息;短视频是竖屏、封面带动态效果、要算 Feed 流权重、允许循环播放。混在一张表里,要么产生一堆空字段,要么用 JSON 字符串存扩展信息导致查询没法走索引。我倾向于拆两张表,各自带着自己的业务字段,查询时互不干扰。

CREATE TABLE `vod_video` ( `id` bigint NOT NULL AUTO_INCREMENT, `title` varchar(128) NOT NULL COMMENT '标题', `cover_url` varchar(512) DEFAULT NULL COMMENT '封面图', `video_url` varchar(512) DEFAULT NULL COMMENT '原始文件地址', `hls_url` varchar(512) DEFAULT NULL COMMENT '转码后 HLS 地址', `duration` int DEFAULT 0 COMMENT '时长,单位秒', `category_id` bigint DEFAULT NULL COMMENT '分类', `status` tinyint DEFAULT '0' COMMENT '0-草稿 1-转码中 2-正常 3-已下线', `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='点播视频表'; CREATE TABLE `short_video` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint NOT NULL COMMENT '作者', `title` varchar(128) DEFAULT NULL, `cover_url` varchar(512) DEFAULT NULL, `play_url` varchar(512) DEFAULT NULL COMMENT '播放地址', `width` int DEFAULT 1080 COMMENT '原始视频宽', `height` int DEFAULT 1920 COMMENT '原始视频高', `feed_weight` int DEFAULT 0 COMMENT '推荐权重', `status` tinyint DEFAULT '0' COMMENT '0-草稿 1-转码中 2-正常 3-已下线', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_feed` (`status`, `feed_weight`, `id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='短视频表';

注意两个字段命名的细节:短视频表里我写的是play_url而不是video_url,因为短视频最终播放可能指向 HLS 播放串,也可能直接指向压缩后的 MP4,叫 play_url 更贴切;点播表里保留video_url存原始文件,hls_url存转码产物,是为了方便排查转码链路到底断在哪一步。状态机的流转是:上传时草稿 → 转码接口拉取后置为转码中 → 转码回调成功后置为正常 → 运营手动下线。用status字段的数值顺序,可以在列表页直接WHERE status = 2,不用在业务里再做一遍状态映射。

3.2 社区帖子与评论:root_id 楼层模型避免递归查询

社区交流的核心是帖子流和评论。帖子表需要关联视频或短视频,我用两个字段media_type和media_id做多态关联——media_type=1表示关联点播视频,media_type=2表示关联短视频,media_type=0表示纯图文帖。这样既保留灵活性,又不会在 join 时把不同表的数据混进来。

CREATE TABLE `post` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint NOT NULL, `content` text COMMENT '正文', `media_type` tinyint DEFAULT '0' COMMENT '0-纯图文 1-点播 2-短视频', `media_id` bigint DEFAULT NULL, `audit_status` tinyint DEFAULT '0' COMMENT '0-待审核 1-通过 2-拒绝', `like_count` int DEFAULT '0', `comment_count` int DEFAULT '0', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_audit` (`audit_status`, `id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='社区帖子表'; CREATE TABLE `comment` ( `id` bigint NOT NULL AUTO_INCREMENT, `post_id` bigint NOT NULL COMMENT '所属帖子', `root_id` bigint DEFAULT '0' COMMENT '顶层评论 ID,0 表示顶层评论', `parent_id` bigint DEFAULT '0' COMMENT '直接父评论 ID', `user_id` bigint NOT NULL, `reply_user_id` bigint DEFAULT NULL COMMENT '被回复人', `content` varchar(512) NOT NULL, `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_post_root` (`post_id`, `root_id`, `id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='帖子评论表';

这套设计的核心是root_id冗余。查询某个帖子下的全部评论时,只需要WHERE post_id = ? AND root_id = ?按整楼取出,再在内存里按parent_id组装成树。如果只存parent_id,每次查楼层都要递归或者用 MySQL 8 的 WITH RECURSIVE,评论一多查询就明显变慢,而且深度不可控。reply_user_id是为了在前端显示「回复 @某某」,省得再查一次用户表。

点赞、收藏、关注这类的互动关系,不建议每种都建一张表。建一张通用关系表user_id + target_type + target_id + action + create_time,把点赞、收藏、浏览记录都丢进去,加唯一索引防止重复点。这里唯一索引建议用(user_id, target_type, target_id),action 字段本身不需要进唯一约束,因为一个用户对同一个内容只能有一种点赞状态,再有取消点赞的操作就靠单独字段或状态值记录。

3.3 抢购与拍卖:库存快照、活动时间、出价记录的字段陷阱

抢购模块涉及三张表:活动主表、活动商品表、订单表。活动主表记录名称和起止时间;活动商品表记录每个商品在本次活动里的抢购价和抢购库存;订单表单独落一张,方便统计活动销量,避免和普通订单混在一起。下面这两张表是抢购的核心,我加上了关键索引。

CREATE TABLE `flash_sale_sku` ( `id` bigint NOT NULL AUTO_INCREMENT, `activity_id` bigint NOT NULL, `goods_id` bigint NOT NULL, `flash_price` decimal(10,2) NOT NULL COMMENT '抢购价', `stock` int NOT NULL COMMENT '活动库存,预热后读 Redis', `locked_stock` int DEFAULT '0' COMMENT '锁定库存,异步回补用', `limit_per_user` int DEFAULT '1' COMMENT '每人限购', `sale_out` tinyint DEFAULT '0' COMMENT '是否售罄', PRIMARY KEY (`id`), KEY `idx_activity` (`activity_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='抢购活动商品表'; CREATE TABLE `auction` ( `id` bigint NOT NULL AUTO_INCREMENT, `goods_name` varchar(128) NOT NULL, `cover_url` varchar(512) DEFAULT NULL, `start_price` decimal(10,2) NOT NULL COMMENT '起拍价', `current_price` decimal(10,2) NOT NULL COMMENT '当前价', `current_user_id` bigint DEFAULT NULL COMMENT '当前最高出价人', `min_increment` decimal(10,2) DEFAULT '0.00' COMMENT '最小加价幅度', `start_time` datetime NOT NULL, `end_time` datetime NOT NULL, `status` tinyint DEFAULT '0' COMMENT '0-进行中 1-已成交 2-流拍', PRIMARY KEY (`id`), KEY `idx_status_time` (`status`, `end_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='拍卖表'; CREATE TABLE `auction_bid` ( `id` bigint NOT NULL AUTO_INCREMENT, `auction_id` bigint NOT NULL, `user_id` bigint NOT NULL, `bid_price` decimal(10,2) NOT NULL, `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_auction_price` (`auction_id`, `bid_price`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='拍卖出价记录表';

字段陷阱主要在三个地方。第一,金额类型必须用decimal,不要用float或double,1.1 这种数字在二进制里本来就存不精确,交易场景下差一分钱都会被用户投诉。第二,拍卖结束时间建议直接用datetime,但出价接口里对比时要用毫秒级时间戳,避免在同一秒内两个出价都判断「未结束」。第三,auction表里必须有一个current_price字段,它是乐观锁的版本依据,第 5 章会专门讲怎么用它防并发冲突。如果你后续要做拍卖保留价,再加一个reserve_price字段就可以,不影响主流程。

4. 视频模块落地:从上传、转码、HLS 播放到直播与短视频 Feed 流

4.1 点播与短视频:自建转码链路与播放凭证签发

视频点播的完整链路是:上传源文件 → 转码 → 输出 HLS 切片 → 播放。自建方案用 FFmpeg 做转码,输出.m3u8索引文件和一堆.ts分片,存到对象存储或本地磁盘。转码是异步的,后端提交任务后要立刻返回,不能把请求堵在转码完成上。等转码完成后,通过回调接口或消息通知,把视频状态从「转码中」改成「正常」,并回填hls_url。

另一个必须做的点是播放地址签名。直接暴露 HLS 地址,被爬虫抓一次就能无限转发;更常见的是别人拿到地址直接下载源文件。我一般会给播放地址加一道时间戳签名:

public String buildPlayUrl(String hlsPath, long expiresSeconds) { long expireAt = System.currentTimeMillis() / 1000 + expiresSeconds; String raw = hlsPath + ":" + expireAt + ":" + secretKey; String sign = DigestUtils.md5Hex(raw); return cdnBase + hlsPath + "?expires=" + expireAt + "&sign=" + sign; }

签名逻辑就是把 URL 路径、过期时间、密钥拼在一起算 MD5,服务端在播放接口里重新算一遍然后比对,超过过期时间就拒绝。expiresSeconds点播建议 900 秒,短视频可以更短,比如 300 秒,降低盗链风险。如果你买了云厂商的点播服务,这些能力平台都内置了,你只要调 API 签个播放凭证,后面转码、存储、切片全在云上,团队省去一套 FFmpeg 运维;代价是它像一个黑匣子,出问题只能看平台日志,没法深入排查。自建方案则反过来,一切可控但所有排查都得自己做,适合愿意折腾、有服务器资源的团队。短视频如果源文件本身就是压缩好的 MP4,也可以不转码直接播放,首屏加载会略微影响体验,但能省大量转码资源。

4.2 直播:协议选型、小程序播放与成本权衡

直播链路由三段组成:推流端采集画面 → 流媒体服务接收并转发 → 播放端拉流。推流端最常见的是 OBS,推流协议用 RTMP;流媒体服务可以用 SRS 或 Nginx-RTMP 插件;播放端根据协议不同,延迟和兼容性差异很大。我先把协议对比列出来,方便你按场景选:

协议延迟小程序端支持适用场景
RTMP1-3 秒仅推流端使用采集推流
HTTP-FLV1-3 秒live-player 支持直播观看
HLS5-15 秒live-player 支持弱网环境更稳
WebRTC1 秒以内部分场景支持连麦、互动直播

常见做法是推流用 RTMP,拉流用 HTTP-FLV,这样延迟控制在 3 秒内,普通用户感知不到太明显的卡顿。如果你的目标用户很多人在弱网环境,可以把拉流协议切成 HLS,兼容性最好但延迟会明显增加;直播延迟是个玄学,影响它的是推流设置了纠错缓冲、播放器缓冲、CDN 节点命中情况,前端的buffer-time参数和网络状况对延迟影响比服务端还大。

小程序端播放直播用live-player组件,这里有一个合规门槛必须提前知道:live-player在微信小程序里属于类目敏感能力,个人主体无法开通,需要企业主体并完成直播资质审核。如果你只是做源码演示,可以在本地跑通推拉流,但真机预览时常会遇到黑屏,多半不是代码问题,而是类目和域名没配置好。播放域名必须是 HTTPS,且在微信公众平台后台配进 request 合法域名。

管理后台看直播录像或者点播回放时,Vue 里播放 m3u8 必须引入 hls.js,因为原生video标签只认 MP4 和 WebM。这一段代码是管理后台通用的播放器封装:

<template> <video ref="videoRef" controls></video> </template> <script setup> import Hls from 'hls.js' const videoRef = ref(null) const playM3u8 = (url) => { if (Hls.isSupported()) { const hls = new Hls() hls.loadSource(url) hls.attachMedia(videoRef.value) hls.on(Hls.Events.MANIFEST_PARSED, () => { videoRef.value.play() }) } else if (videoRef.value.canPlayType('application/vnd.apple.mpegurl')) { videoRef.value.src = url } } </script>

直播间的互动评论,能用轮询就不要急着上 WebSocket。3 秒轮询一次直播间的评论,对几千人同时在线的场景都够用,代码简单、链路稳定;等在线人数真的上到十万,再去考虑 WebSocket 通道和消息持久化。这个取舍能让你省掉一大块复杂度,也不会被老板说「直播间没互动感」。

4.3 短视频 Feed 与社区信息流:游标分页而不是页码分页

短视频 Feed 和社区信息流是同一个问题:用户无限往下滑,每次加载更多。最错误的分页写法是pageNo * pageSize的 OFFSET 分页,数据一旦到了几万条,OFFSET 每次都要从头数,查询时间越来越长,用户体验就是「滑着滑着突然卡住」。正确做法是游标分页:以上一次拿到的最后一条数据 ID 作为下一次的起点。

SELECT id, title, cover_url, play_url FROM short_video WHERE status = 'normal' AND id < #{cursor} ORDER BY id DESC LIMIT #{pageSize}

接口返回list和nextCursor,前端触底时把nextCursor带给接口,下拉刷新时清空游标重新查首屏。pageSize建议一次 10 到 20 条,短视频信息密度高,给太多反而让用户刷得疲劳。

社区帖子 Feed 的做法类似,但要注意一件事:帖子流和短视频流不要内容重复。短视频 Feed 只查short_video表,社区 tab 只查post表,帖子里的media_type=2关联了短视频,此时在社区 tab 里展示的是帖子的正文加视频卡片,和短视频 Feed 的单视频流观感不同,用户不会觉得重复。如果嫌查询时 join 视频表麻烦,也可以在前端拿到帖子列表后根据media_id去批量取视频信息,但更推荐写一个postjoin 视频的查法:先分页查post表,再按media_idIN 查询短视频表,两段代码都不复杂,还能避免 N+1 查询。

5. 抢购与拍卖的高并发设计:库存扣减、出价锁与 5 条避坑记录

5.1 抢购:Redis Lua 原子扣库存,数据库负责记账

抢购是所有模块里最容易翻车的地方。最早的方案是「先查库存再扣库存」,用SELECT stock判断大于 0 再UPDATE stock = stock - 1。这个写法在单线程下没问题,一旦 5000 人同时抢一个商品,多个请求都读到剩 1 件库存,然后一起执行 UPDATE,瞬间产生十几个订单——这就是超卖。修复方案很简单:让库存扣减变成原子操作。

常见做法是把库存预热到 Redis,用 Lua 脚本保证「判断库存 + 扣减库存」两步不可分割:

if (tonumber(redis.call('get', KEYS[1]) or 0)) >= tonumber(ARGV[1]) then redis.call('decrby', KEYS[1], ARGV[1]) return 1 else return 0 end

Java 端用 RedisTemplate 执行这段脚本:

DefaultRedisScript<Long> script = new DefaultRedisScript<>(luaText, Long.class); Long ok = redisTemplate.execute( script, Arrays.asList("flash:stock:" + activityId + ":" + skuId), String.valueOf(buyNum) ); if (ok != null && ok == 1L) { // 允许下单,异步写订单 }

这个方案的要点有三个。第一,库存预热要在活动开始前完成,我一般提前 10 分钟就把活动商品库存从数据库读到 Redis,确保 Redis 里不会有「冷启动导致 0 库存」的误判。第二,写订单这一步要异步化,用 Spring 的@Async或者一个独立线程池接收消息秒级落库,不要让用户的 HTTP 请求一直阻塞在数据库写入上。用户只要看到「下单成功」的反馈就够了,订单最终落库是后端的事。第三,flash_sale_order表要加(user_id, activity_id, sku_id)唯一索引,防止用户在 Redis 扣减成功后快速点了两次提交,生成了两个订单。如果订单超过 15 分钟未支付,后台补偿任务要把订单标记取消,并把 Redis 库存回滚,这个回滚不是简单incrby,要先比对数据库未支付订单数,避免多还库存。

5.2 拍卖:用乐观锁推进当前价,别给拍卖行上悲观锁

拍卖和抢购的并发问题不一样。抢购是「扣库存」,拍卖是「更新当前最高价」。最容易犯的错是在出价接口里先查询当前价,再更新当前价,两个步骤中间插入几个并发请求就乱了:两个人同时读到当前价 100 元,都出 110 元,后更新的把先更新的覆盖掉,但两个人都看到了「出价成功」。

标准解法是用条件更新代替「查一次再更新」:

UPDATE auction SET current_price = #{bidPrice}, current_user_id = #{userId}, updated_at = NOW() WHERE id = #{auctionId} AND current_price = #{expectedPrice}

先查出当前价作为expectedPrice,然后执行这条 SQL。如果影响行数是 1,说明在更新前没有人改过价格,出价成功;如果影响行数是 0,说明有人在你这几次操作之间抢出了更高价,本次出价失败,前端重新拉取最新价后让用户再出一次。这把「校验 + 更新」合并成一条 SQL,天然原子。

为什么不上悲观锁?SELECT ... FOR UPDATE会锁住这行记录,同一拍品的出价请求全部串行排队,并发量一大请求就堆积,等待时间超时,用户端表现就是「点了没反应」。拍卖场景里绝大多数出价都会成功,直接乐观锁失败重试的成本远低于悲观锁的等待成本。另外,出价接口里也要判断当前时间是否已经超过end_time,这个判断可以直接放在UPDATE的 WHERE 条件里:

AND end_time > NOW(3)

这样可以减少一次「查询拍卖信息」的接口调用,也让截止时间判断和价格更新处于同一原子操作。

5.3 避坑清单:抢购与视频模块最容易翻车的 5 个点

我在这个项目方向上踩过不少坑,大部分是上线后才暴露的。挑 5 个高频问题列在这里,每一条都按「现象 → 原因 → 解决」写清楚:

  1. 现象:抢购开始后数据库库存变负数。 原因:代码先 select 判断再 update,多个请求同时通过检查,超卖。 解决:改成 Redis Lua 原子扣减,数据库只负责记录订单;如果不引入 Redis,就用UPDATE ... SET stock = stock - 1 WHERE stock > 0,靠数据库行锁兜底。

  2. 现象:拍卖两个人同时出价,两个人都收到成功提示。 原因:先查价再更新价,两个请求都基于同一旧价,后写覆盖先写。 解决:用UPDATE ... WHERE current_price = 期望价的条件更新,影响行数为 0 时返回失败让用户重试。

  3. 现象:直播推流正常,小程序端黑屏或一直缓冲。 原因:小程序拉流要求 HTTPS 合法域名 + 直播类目资质;HLS 分片在弱网下首屏加载慢。 解决:播放域名配到微信公众平台合法域名,审核类目在开发前先确认;拉流优先 HTTP-FLV,真机预览代替开发者工具验证。

  4. 现象:视频转码后状态一直停在「转码中」。 原因:转码服务没有回调后端接口,或者回调地址写错,后端一直等不到状态更新。 解决:实现转码回调 HTTP 接口,回调成功就更新status=2和hls_url;同时检查源文件是否损坏,损坏的要自动标记失败并告警。

  5. 现象:管理后台视频播放不了,浏览器提示需要装插件。 原因:原生video标签不认 m3u8 格式,HLS 必须用播放器库解析。 解决:在 Vue 里封装一个带 hls.js 的播放组件,点播、短视频、直播回放全部复用,不要把播放逻辑散落在各个页面。

这五条里前两条是并发算法的硬错误,后三条是视频和直播链路的系统工程问题,都不难修,但提前做好了能省掉大量半夜排障的时间。

6. 前后端联调与上线自测:统一响应体、小程序长列表与端到端验证

6.1 统一 Response 与 axios 拦截器

这个项目横跨点播、直播、社区、交易六个模块,后端接口数量轻松上百。如果每个接口返回结构不一致——有的直接返回数组,有的套一层{data: ...},前后端联调一定会因为各种边界问题反复扯皮。我习惯在一开始就定死统一响应体:{ code: 0, message: "ok", data: ... },code 为 0 表示成功,非 0 表示业务错误,HTTP 状态码只留给系统级错误。

Vue 管理端把 axios 封装一次,所有页面都走同一个入口:

const instance = axios.create({ baseURL: '/api', timeout: 15000 }) instance.interceptors.response.use( res => { if (res.data.code === 0) return res.data.data ElMessage.error(res.data.message || '请求失败') return Promise.reject(new Error(res.data.message)) }, err => { if (err.response && err.response.status === 401) { router.push('/login') } return Promise.reject(err) } ) export default instance

这段代码的关键是return res.data.data,这样页面里调用getXXX()直接拿到业务数据,不用每个组件里再去解包res.data.data.data。统一之后,新接手的同事只要照着写接口函数就行,不需要关心响应结构。

6.2 小程序长列表与视频懒加载的配置

小程序端的关键性能点在长列表和视频加载。短视频列表如果一次渲染上百个video组件,小程序直接卡死,因为每个 video 组件都会抢占解码资源。常见做法是列表只渲染当前屏幕附近的数据,用wx:if控制当前播放项,其他项用cover封面图占位。video组件要设preload="none",让它在屏幕外时不加载数据,只有滑动到接近时才设置真正的播放地址。

分页接口用上一章说的游标方式,小程序端配合onReachBottom触底事件,每次把nextCursor传上去,就能做到无限加载不卡顿。顶部导航栏高度也是一个容易踩的小细节:不同机型状态栏高度不一样,写死 44px 会在刘海屏上露出白边。用wx.getWindowInfo()动态获取statusBarHeight再设置padding-top,才能保证真机上不偏。

最后,我自己每次交付这种视频 + 交易类型的项目,上线前一定会拿测试号走一遍完整闭环:注册 → 看短视频 → 进直播间发一条评论 → 抢购下单 → 拍卖加价 → 管理后台看到对应订单。如果这条链路不断,剩下的就是流量和运营问题。尤其是从零开始搭这类系统的人,先把直播资质和视频存储成本预算好,再谈功能,不然很容易做一半翻车。希望这些方案对你有帮助。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询