每年毕业季都会有不少学生来问我计算机毕业设计到底选什么题。虚拟股票交易系统这个题目我前前后后见过很多人做自己也用 Java SpringBoot 完整搭过一个 Web 版模拟股票交易平台作为教学实训系统来讲相当能打。第一句话先说结论这个题选得不错——业务逻辑够复杂复杂度又刚好卡在本科毕设的射程内往外能聊金融业务往里能聊并发、事务、定时任务答辩时非常出活。这篇内容会按我实际做这个毕设项目的思路来写从需求拆解、技术选型、数据库设计到交易撮合核心逻辑、前端实时行情展示再到部署演示和避坑经验。如果你是正在准备计算机毕业设计的学生或者想拿 SpringBoot 练一个完整 Web 项目这篇文章基本可以当一份开发笔记用。1. 这个项目到底在做什么需求拆解与整体设计思路1.1 虚拟股票交易系统的业务本质很多人第一反应是这不就是一个带增删改查的管理系统吗其实不是。虚拟股票交易系统最核心的地方在于它有一个“市场实时变化”的维度整个系统围绕“用户拿虚拟资金买卖股票”这个闭环展开。完整的业务链路是这样的用户注册登录 - 系统给一笔虚拟资金我通常设计成 100 万初始资金- 用户浏览行情 - 选择某只股票下单买入 - 系统冻结资金 - 撮合成交 - 更新持仓和可用资金 - 用户卖出 - 系统冻结股票 - 撮合成交 - 回笼资金。整个过程还要记录每一笔资金流水、每一笔成交记录并实时计算总资产和浮动盈亏。这就意味着它不是普通的管理系统而是一个带状态的、有资金约束的撮合交易模拟器。用生活化的类比来说它像一个“模拟经营游戏”给你一筐金币让你在虚拟市场里买卖赢了算你的本事亏了也只是账面上的数字。这个项目适合谁来写一是计算机专业毕业生二是想做课程设计的学生三是想系统练习 SpringBoot 全栈开发的初学者。它能覆盖的技术点足够多SpringBoot、MyBatis、MySQL、WebSocket、定时任务、安全框架、前端图表库这些都是简历上常见的热词。1.2 为什么选 Java SpringBoot 而不是其他组合毕设选题的一个关键原则是不要为了炫技选一个自己完全hold不住的架构也别选一个一两周就能写完的CRUD空壳。Java SpringBoot 恰好卡在中间属于“体面且可控”的选择。SpringBoot 最大的优势是约定大于配置。以前用 SSMSpring SpringMVC MyBatis搭项目光是 XML 配置就能写半天SpringBoot 内置 Tomcat一个 main 方法就能启动依赖管理靠 starter 一键引入这对时间紧张的毕设党太友好了。有人会问现在微服务这么火要不要用 Spring Cloud我的建议是不要。毕业设计的核心是展示你对一个完整系统的理解而不是堆砌中间件。一个虚拟股票交易系统单机部署就能跑强行拆成微服务只会增加调试成本和答辩风险。又有同学问要不要用前后端分离这个可以根据自己的前端水平决定。如果你熟悉 Vue前后端分离展示效果确实好如果时间不够服务端渲染也能交差后面我会详细对比。1.3 功能模块划分与核心流程做毕设最先要做的是把模块画出来不然代码会越写越乱。我做的时候把系统分成六大模块用户模块注册、登录、个人信息、虚拟资金充值模拟充值行情模块股票列表、实时行情、分时图、K线图交易模块买入、卖出、委托管理、撤单资产模块资金账户、持仓列表、盈亏分析、资金流水数据模块模拟行情生成器、历史K线生成管理模块用户管理、股票管理、系统参数配置可选但加了很加分核心流程需要强调一点委托和成交是两回事。用户点击“买入”系统先创建一条委托单然后进入撮合流程撮合成功后才生成成交记录。很多新手直接把点击买入等同于买入成功这是不对的后面数据库设计的时候就会体现这个区别。2. 技术选型背后的逻辑与数据库设计2.1 后端依赖与基础配置我用的是 SpringBoot 2.7.x这个版本生态成熟网上资料多遇到问题容易搜到答案。SpringBoot 3.x 已经全面转向 jakarta 命名空间很多旧教程和依赖配置对不上毕设阶段没必要给自己挖这个坑。核心依赖就这几样spring-boot-starter-web、mybatis-plus、mysql-connector-java、lombok、spring-boot-starter-validation、spring-boot-starter-websocket。接口文档工具我习惯用 knife4j比原生 Swagger 好看且方便答辩演示的时候可以现场调接口给老师看非常加分。这里有一个容易踩的坑MyBatis-Plus 的版本要和 SpringBoot 对得上。我自己就遇到过启动报错原因是 mybatis-plus 版本太老不兼容当前的 SpringBoot 版本。建议直接选较新的 3.5.x 版本然后跑一个最简单的查询接口确认环境通了你再往下写。2.2 数据模型设计用户、资金、持仓、订单、成交数据库是整个系统的地基。我设计的时候一共用了八张核心表user用户表字段有 id、username、password、phone、role普通用户/管理员、create_timeuser_account资金账户表字段有 user_id、total_balance总资产、available_balance可用资金、frozen_balance冻结资金stock股票基础信息表字段有 stock_code、stock_name、stock_industry、initial_price、current_price、status是否停牌stock_quotation实时行情快照表字段有 stock_id、latest_price、open_price、high_price、low_price、volume、涨跌幅、update_timetrade_order委托单表这是核心字段有 id、user_id、stock_id、order_type买入/卖出、price、quantity、filled_quantity、status、create_timetrade_record成交记录表字段有 order_id、user_id、stock_id、trade_price、trade_quantity、total_amount、trade_timeuser_position用户持仓表字段有 user_id、stock_id、available_quantity可用数量、frozen_quantity冻结数量、average_cost持仓成本capital_flow资金流水表记录每一次资金变动字段有 user_id、amount、flow_type充值/买入扣款/卖出回款、balance_after、remark这八张表基本能覆盖所有业务场景。有一点要特别说明trade_order 一定要有 status 字段。我定义的是0 待成交、1 部分成交、2 全部成交、3 已撤单。部分成交是最容易被忽略的状态——如果用户买了 1000 股但市场上只有 300 股的卖单那剩下的 700 股还在挂着。很多第一次做交易系统的同学没考虑这种情况导致持仓数量和资金状态对不上。2.3 模拟行情生成器让数据“活”起来虚拟交易系统没有真实行情源所以必须有一个模拟行情生成器这是系统能不能“看着像真的”的关键。我的方案是随机游走模型 均值回归。具体做法是给每只股票设置一个初始价格每隔几秒钟产生一个新的价格新价格等于旧价格加上一个随机扰动项同时让价格不会无限涨跌而是围绕一个基准价格波动。核心思路用伪代码描述是这样的// 模拟价格生成逻辑 double randomWalk (Math.random() - 0.5) * 2; // 区间 [-1, 1] double drift (basePrice - latestPrice) * 0.05; // 均值回归力度 double newPrice latestPrice * (1 randomWalk * 0.01 drift);这个逻辑的好处是行情看起来自然既不会一直死水一潭也不会短时间内暴涨暴跌太多。定时任务我用 Spring 自带的 Scheduled 来跑每 3 到 5 秒刷新一次行情并把最新行情保存到 stock_quotation 表同时通过 WebSocket 推送给在线用户。需要注意的是模拟股票数量不用太多50 到 80 只足够演示。股票太多会拖慢行情刷新速度而且数据库压力大。每只股票的行业、名称、代码要尽量编得像模像样比如“虚拟科技”“模拟银行”“实验医药”降低演示时的出戏感。3. 核心业务实现从登录到买卖成交的完整链路3.1 用户注册登录与安全设计用户模块看起来简单但安全这块不能偷懒。密码不能明文存数据库我用的是 BCrypt 加密Spring Security 的内置组件可以很方便做到。登录方式我建议用 JWTJSON Web Token。用户在登录接口校验通过后后端返回一个 token前端后续请求在请求头里带上 token后端通过拦截器统一校验。这么做既安全又能在答辩时讲出“无状态认证”这个加分点。权限控制上普通用户和管理员要分开。普通用户可以充值、交易、查资产管理员可以管理股票、查看所有用户列表。用拦截器 角色字段就能实现不需要上复杂的安全框架。项目中我建议加一个简单的自定义注解来标记哪些接口需要管理员权限代码更干净。3.2 交易核心委托单处理与撮合引擎这是整个系统的心脏也是答辩时老师最可能追问的地方。一个合格的虚拟撮合引擎至少要处理三件事下单校验、资金/股票冻结、成交更新。下单校验分两种情况买单计算冻结金额 委托价格 * 委托数量校验可用资金是否充足卖单校验可用持仓数量是否充足如果不足直接拒绝这里有个细节资金计算必须用 BigDecimal不能用 double 或 float。股票价格的小数位、数量的乘法用 double 会出现 0.30000000000000004 这种精度问题答辩现场翻车非常尴尬。下单通过后进入撮合逻辑。我实现的是价格优先、时间优先的简化版撮合// 以买单为例找到价格 当前买单价格、且时间更早的卖单 ListTradeOrder sellOrders getSellOrders(stockId, order.getPrice()); for (TradeOrder sellOrder : sellOrders) { if (order.getFilledQuantity() order.getQuantity()) { break; // 当前买单已全部成交 } // 按对手价成交买方以卖方挂单价格成交 int fillQuantity Math.min(order.getRemainingQuantity(), sellOrder.getRemainingQuantity()); // 成交价格可以取卖单价格也可以取两者的中间价 BigDecimal dealPrice sellOrder.getPrice(); // 执行成交生成 trade_record、更新资金和持仓 executeMatch(order, sellOrder, dealPrice, fillQuantity); }需要实现 executeMatch 里的事务逻辑。一个成交动作涉及多个表更新trade_order 的已成交数量、trade_record 插入记录、user_account 的余额变动、user_position 的持仓变动。这些操作必须放在同一个事务里任何一个环节失败都要整体回滚。还有一个非常关键的业务点冻结和解冻。用户下买单后资金要先冻结成交后扣减冻结资金剩余未成交部分要么继续挂着要么用户主动撤单解冻。如果不做冻结用户同时下多笔订单资金可能被超扣这在答辩演示时是会直接翻车的。撮合引擎有两种做法。一种是像我上面说的数据库里查对手单然后循环撮合逻辑清晰适合毕设。另一种是抢单即时成交——用户下卖出单时按当前最优买价立即成交不需要挂单等待。我建议选择一种做主逻辑即可不要试图把所有模式都实现会把工作量推高到不合理的水平。3.3 资产与持仓管理盈亏计算与记录查询资产页面是用户最直观感受到“赚钱还是亏钱”的地方。总资产的计算公式是总资产 可用资金 持仓市值 持仓市值 Σ(持仓数量 × 最新价) 浮动盈亏 Σ((最新价 - 持仓成本) × 持仓数量)这部分要注意的是持仓均价的计算。用户多次买入同一只股票持仓成本会变。比如第一次 10 元买 100 股第二次 12 元买 100 股持仓成本就是 (1000 1200) / 200 11 元。这个计算要在每次买入成交的时候更新到 user_position 表而不是成交记录里临时算。卖出的时候要处理一个问题卖出的是持仓中的哪一部分我采用最常用的移动加权平均法卖出时不改变持仓成本只减少持仓数量。比如上面那只股票卖 50 股持仓成本仍然是 11 元剩下 150 股的持仓成本不变但卖出回笼的资金要按成交金额入账。资金流水表记录每一笔资金变动充值、买入扣款、卖出回款都要记录。答辩时老师很可能会问“用户的每一笔钱你都追踪得到吗”有了这张表你把用户的资金流水拉出来从注册到交易到当前余额每一笔都对得上这是最有力的回答。4. 前端界面与交互Web版实训系统的体验设计4.1 前端选型模板渲染还是前后端分离虚拟股票交易系统的前端我见过两种主流做法第一种是 Thymeleaf Bootstrap服务端渲染页面。SpringBoot 直接返回 HTML学习成本低不需要单独启动 Node 服务。缺点是页面交互做起来比较笨重实时行情感知较弱。第二种是 Vue Element UI前后端分离。前端通过 API 调用后端接口页面更现代化有股票软件的质感。缺点是工程结构更复杂部署时需要分别处理前端静态文件和后端 jar 包。我的建议是如果毕业设计时间充裕选 Vue。因为视觉效果好答辩演示能直接拿高分。如果对前端不熟Thymeleaf 也可以但一定要在页面交互上下点功夫别搞得像后台管理系统。如果选了 Vue最终部署时可以执行 npm run build把生成的 dist 目录放到 SpringBoot 的 static 目录下这样就打成一个 jar 包了前端和后端一起启动不会出现“前端页面跑不起来只能给老师看接口”的尴尬。4.2 核心页面与实时刷新方案系统页面我分成五个主要页面登录注册页、行情列表页、股票详情页、交易页、资产页。行情列表页是整个系统的门面。建议做成一个表格展示股票代码、名称、现价、涨跌幅、最高最低、成交量等字段。涨红跌绿红色表示涨绿色表示跌这是股票行业通用配色别搞反了。实时刷新是 Web 版虚拟交易系统的一个重要体验点比轮询更优的方案是 WebSocket。后端每 3 秒生成一批新行情后通过 WebSocket 推送给前端前端收到消息后局部更新页面数据不需要整页刷新也不会有频繁请求打爆后端的风险。具体实现前端用 wx.connect 建立连接后端用 ServerEndpoint 定义 WebSocket 接口。我的做法是前端维护一个全局的股票行情 Mapkey 是 stock_idvalue 是最新报价对象。收到 WebSocket 推送后遍历当前表格绑定的数据源找到对应行更新价格和涨跌幅再触发 Vue 的响应式更新。这种方式即使有几十只股票同时刷新也不会卡。4.3 图表与K线展示方案股票详情页需要一个 K 线图我选择的是 ECharts。ECharts 自带股票 K 线图的 series-k 类型用起来非常方便。后端要提供一个 K 线数据接口返回某个时间范围内的开盘价、收盘价、最高价、最低价数组。前端拿到数据后组装成 ECharts 需要的格式// ECharts K线数据格式4个维度分别对应 开盘、收盘、最低、最高 // 注意顺序ECharts 要求 [open, close, lowest, highest] const klineData records.map(item [ item.openPrice, item.closePrice, item.lowPrice, item.highPrice ]);数据时间维度我分成两类分时图显示当天价格走势K线图显示日线级别数据。模拟系统没有真实历史数据所以 K 线可以从系统启动那天开始积累也可以在初始化数据时用脚本生成过去 60 个交易日的模拟日 K这样打开页面就有历史可看演示效果好。5. 实操过程从0到1搭建并运行整个项目5.1 项目初始化与目录结构我把整个项目的搭建步骤拆成了清晰的几步。第一步是用 Spring Initializr 创建项目选择 Java 8 或 11依赖选择 Web、MySQL、Lombok。创建完成后把 MyBatis-Plus 和 WebSocket 依赖加到 pom.xml。项目包结构我是这样划分的com.example.stock ├── controller/ # 接口层 ├── service/ # 业务逻辑层 ├── mapper/ # 数据访问层 ├── entity/ # 数据库实体 ├── dto/ # 接口数据传输对象 ├── config/ # 配置类WebSocket、跨域、拦截器 ├── common/ # 工具类、统一返回结果 └── task/ # 定时任务行情生成这个结构不算复杂但层次清晰。controller 里只做参数接收和结果返回核心业务放在 service 里数据操作全走 mapper。千万要避免把一堆业务逻辑写在 controller 里这种代码后期自己都看不懂。5.2 行情模块的数据流转实现行情模块的数据流转是系统的“血管”。我用 Scheduled 定时任务来做行情生成cron 表达式设置为每 5 秒执行一次Component public class QuoteGenerateTask { Scheduled(cron 0/5 * * * * ?) public void generateQuote() { // 1. 查询所有股票基础信息 // 2. 根据上一笔行情计算出最新价 // 3. 更新 stock_quotation 表 // 4. 将最新行情推送至 WebSocket } }开场时可以先跑一段历史数据生成逻辑把每只股票的过去 60 日K线和最近一笔实时行情初始化好。后面定时任务只负责生成最新价这样数据库里既能看到历史走势也能看到实时变化。有一个实践中的细节行情表只保留最新快照历史K线单独存一张表不要在 stock_quotation 中堆积历史数据。否则一张表的数据量会越来越大页面查询越来越慢而且根本没有必要。5.3 部署与演示注意点项目完成后部署和演示也是需要提前练习的。打包命令很简单mvn clean package -DskipTests打包成功后target 目录下会生成一个 jar 文件用 java -jar 就能跑起来。如果你的前端是 Vue记得先 npm run build 把产物放进 static 目录再打包。演示前的准备工作很重要我吃过亏。一定要准备一个“演示专用账号”里面已经有一些持仓、几笔盈利的交易记录资金水位不是初始值。你想想答辩的时候打开页面用户资产是空白、持仓为零老师一眼就能看出系统是刚初始化的会留下“没有实际验证过”的印象。另外一个非常实用的技巧在答辩演示前先把模拟行情定时任务跑起来十几分钟让行情数据有一些历史波动打开页面时图表就有曲线可以看。不要一启动就马上演示那时页面图表上空荡荡的效果非常差。6. 常见问题与排查技巧实录6.1 事务与并发重复下单、资金超扣我在实际开发中遇到频率最高的问题就是资金和持仓在并发场景下对不上。最典型的场景用户可用资金有 1 万同时快速点了两次买入 8000 元的单。如果两个请求同时走到资金校验逻辑都发现余额够都通过了校验最后两笔订单加起来扣了 1.6 万余额直接变负数。解决方式有两种。第一种是使用数据库的行级锁在查询资金时加上 SELECT ... FOR UPDATE锁定用户账户行让第二个请求等第一个请求提交后再执行// 伪代码Mapper中 select * from user_account where user_id #{userId} for update第二种是乐观锁方式在 user_account 表中加 version 字段更新时校验版本号不匹配则更新失败。我更推荐第一种简单直接而且对这个业务场景来说也不存在锁竞争激烈的问题。卖出持仓的时候也会遇到类似的超卖问题处理思路完全一样。6.2 前端实时刷新踩坑WebSocket 刚调试的时候有一个问题特别隐蔽浏览器连不上或者连上一会儿就断开。常见原因有两个。第一个是 WebSocket 的地址写错了。前端连的地址不是后端接口的 HTTP 地址而是 ws:// 开头的地址且需要包含项目上下文路径。比如 SpringBoot 项目部署在 8080 端口、context-path 是 /stock那 WebSocket 地址应该是 ws://localhost:8080/stock/websocket。很多人漏掉 context-path结果一直连不上。第二个是 Nginx 或者前端代理没配置 WebSocket 支持。如果你用了 Nginx 反向代理必须配置 Upgrade 相关的请求头否则 WebSocket 握手就会失败。本地调试时直接用 vite 代理也要在 proxy 里开启 ws: true。还有一个体验层面的问题推送频率太高会导致页面模板闪烁和浏览器卡顿。行情 3 秒推一次已经足够不要每 500 毫秒推一次前端频繁 setData 会导致移动端白屏Web 端也会明显卡顿。我做过测试5 秒一次最丝滑3 秒一次可以接受1 秒以下明显有体感延迟。6.3 答辩现场容易遇到的问题答辩的时候老师最喜欢追着核心逻辑问其中出现频率最高的问题我列一下每个都能提前准备。至于行情数据来源我选择“定时任务生成模拟行情、基于随机游走模型”这个算法在网上有大量资料可以提前研究好答辩时能讲明白就行。6.4 避坑清单常见问题原因解决方案浮点数计算金额出现小数偏差使用 double/float 计算价格金额金额统一使用 BigDecimal禁用浮点类型资金余额变成负数下单校验与扣款存在并发间隙使用 SELECT FOR UPDATE 行锁或乐观锁用户持仓查出来不准卖出时没有先校验可用持仓卖单下达前校验 available_quantityWebSocket 连接频繁断开地址漏掉 context-path 或代理未开启 ws检查连接地址配置代理 ws: true页面行情不刷新前端没收到 WebSocket 推送检查定时任务是否执行推送消息格式是否一致K线图横轴全是同一天时区设置错误导致日期解析错位统一 date 格式使用 yyyy-MM-dd 格式化打包后前端页面 404没有把构建产物放进 static 目录将 dist 目录内容复制到 static 后重新打包本地运行正常部署后报数据库错误数据库账号密码或时区配置未修改检查 application.yml 数据库连接和字符集做这类系统最值钱的部分不是 CRUD 本身而是把业务闭环想清楚——资金怎么流动、委托怎么撮合、持仓怎么变化、账怎么对平。如果你时间紧先把核心交易链路跑通再往上加图表、定时推送这些加分功能。打磨阶段把精力放在边缘情况和并发一致性问题上面这些都是答辩时老师最容易深挖的点提前准备好会让你从容很多。最后分享一个小技巧一定要准备一个“有内容”的演示账号账号里留几笔历史成交、几只盈利中的持仓、一笔未成交的挂单。答辩演示的时候从资产页面切入通过资金流水把整个交易链路串起来讲一遍老师会直观感觉这个系统是真实跑过、验证过的。祝大家毕业顺利。