这是一个基于 Spring Boot 和 Vue 3 的全栈游戏资产交易平台。项目模拟了一个真实的游戏内市场,支持玩家之间的物品买卖、价格走势分析(K线图)、资产管理以及钱包充值等功能。
- 🎨 沉浸式 UI: 全新设计的动态粒子背景(Particle Network),支持鼠标互动;新增波浪式加载动画,提升视觉流畅度。
- 🔍 高级筛选: 市场页面新增“全部商品”视图,支持按分类(步枪、狙击枪、手枪、刀具)筛选,并提供多种排序方式(价格升/降序、最新发布)。
- 🖼️ 个性化头像: 用户中心支持本地上传自定义头像,打造专属个人资料。
- 📦 库存管理: 优化了背包界面,新增分页功能,轻松管理海量库存物品。
- 实时行情: 首页展示热门推荐商品和 24小时涨幅榜。
- 全局搜索: 支持通过关键词快速搜索特定装备(如 "AWP", "Dragon Lore")。
- 分类浏览: 完整的分类索引和排序工具,帮助用户快速发现心仪商品。
- 视觉体验: 全量覆盖 80+ 种武器皮肤组合的高清预览图。
- 专业图表: 集成 ECharts 绘制实时 K线图,支持查看历史价格走势。
- 深度数据: 实时展示买卖盘口(Order Book),辅助交易决策。
- 快速撮合: 支持限价买入/卖出,系统自动匹配最优价格进行撮合。
本项目的“交易”可以理解为:
- Maker(挂单方):提交限价单,进入订单簿等待成交。
- Taker(吃单方):点击盘口中的某个挂单,按该挂单的价格立即成交(也会被实现为一张“对手方向的新订单”,再交给撮合引擎处理)。
整体链路:HTTP API → 订单/资金/库存预占 → 内存撮合(OrderBook)→ 交易结算(DB 原子更新)→ 通知 + SSE 刷新。
为了把“挂单/吃单/成交/撤单”这件事做得既快又不乱,这套交易核心主要用了两类数据结构:内存数据结构(用于快速撮合) + 数据库结构(用于最终一致与可恢复)。
A. 内存撮合:每个商品一张“订单簿”
-
MatchingEngineService.booksByAssetId:ConcurrentHashMap<Integer, OrderBook>- 直观理解:一个大字典(Map),key 是
assetId,value 是这件商品的“订单簿”。 - 作用:不同商品互不影响,撮合可以并行。
- 直观理解:一个大字典(Map),key 是
-
OrderBook(每个 assetId 一个)内部:两本“按规则自动排队”的账本bids(买盘):ConcurrentSkipListMap<OrderKey, BookOrder>(按价格 DESC、时间 ASC 排序)asks(卖盘):ConcurrentSkipListMap<OrderKey, BookOrder>(按价格 ASC、时间 ASC 排序)- 直观理解:
- 买盘像“竞价队列”:谁出价更高谁排前面,同价谁更早下单谁排前面。
- 卖盘像“降价队列”:谁要价更低谁排前面,同价同样按时间先后。
-
bidIndex / askIndex:ConcurrentHashMap<Integer, OrderKey>- 直观理解:为了“撤单”能 O(1) 找到队列里的位置,不用每次都整本翻。
-
OrderBook.lock:ReentrantLock- 直观理解:同一件商品的撮合是“临界区”,一次只允许一个线程在这本订单簿上做“匹配 + 插入/删除”,避免并发把数量扣乱。
B. 事件(Event):撮合与结算解耦的“交接单”
TradeExecution:一笔成交的“最小描述”- 关键字段:
buyOrderId / sellOrderId / price / quantity(外加 maker/taker)
- 关键字段:
TradeExecutedEvent:一批成交的集合(以及可能需要取消的订单)- 直观理解:撮合引擎只负责“算出来应该成交哪些”,把结果打包成事件交给结算模块去“改钱/改库存/写成交记录”。
C. 数据库侧的“预占”字段:防止重复使用资金/库存
wallet:balance+reserved- 直观理解:下买单时先把钱从“可用”挪到“冻结”,避免同一份余额被重复下多笔单。
player_asset:quantity+reserved_quantity- 直观理解:下卖单时先把库存从“可卖”挪到“锁定”,避免同一件库存被重复挂卖。
market_order.reserved_funds- 直观理解:买单在撮合过程中会用掉一部分冻结金额,这个字段记录“这张买单还剩多少冻结可以释放”(例如价格改善导致少花钱)。
以上数据结构合在一起,就能做到:撮合靠内存结构快,结算靠事务落库稳,撤单靠索引结构准,重启靠数据库恢复可用订单簿。
前端交易页在 src/views/Trade.vue,主要调用:
- Maker 下单:
POST /api/trade/orders(前端:src/api/trade.js#createOrder) - 查询我的挂单:
GET /api/trade/pending?userId=...(前端:src/api/trade.js#fetchPendingOrders) - 撤单:
POST /api/trade/cancel/lock(先“锁撤单”,从内存订单簿移除,避免继续被撮合)POST /api/trade/cancel(最终撤单,释放预占资金/库存并落库)POST /api/trade/cancel/unlock(撤单解锁:重新加入订单簿)
- Taker 吃单成交:
POST /api/market/trade(前端:src/api/market.js#executeTrade) - K 线/成交记录:
GET /api/market/history?itemId=...等(前端:src/api/market.js#fetchTradeHistory)
对应后端控制器:
backend/src/main/java/com/gamemarket/controller/TradeController.javabackend/src/main/java/com/gamemarket/controller/MarketController.java
入口:backend/src/main/java/com/gamemarket/service/OrderService.java#createOrder
BUY 限价单(买单)
- 计算总价:
total = price * amount - 调用
WalletService.reserveFunds(playerId, total)把可用余额转入reserved(预占) - 落库
MarketOrder:status=CREATED -> OPEN,并记录reservedFunds=price*amount - 提交到撮合引擎:
MatchingEngineService.submit(order)
SELL 限价单(卖单)
- 校验玩家库存
PlayerAsset是否足够:available = quantity - reservedQuantity - 增加
reservedQuantity(预占库存,避免重复出售) - 落库
MarketOrder:status=CREATED -> OPEN - 提交到撮合引擎:
MatchingEngineService.submit(order)
此外会触发一次“全端刷新”事件:RealtimeEventService.broadcastRefresh()。
你可以把交易系统想象成“菜市场撮合 + 收银台结算”,分两步走:
第一步:先排队(下单/挂单)
- 你下单时,系统先把你的资源“锁住”:
- 买单:先冻结余额(
wallet.reserved增加) - 卖单:先锁定库存(
player_asset.reserved_quantity增加)
- 然后把订单写进数据库(保证可追溯、可恢复)。
- 再把订单放进这件商品对应的“订单簿队列”(内存
OrderBook),等待撮合。
第二步:撮合成交后再结算(改钱/改库存/写成交记录)
- 撮合引擎只做一件事:在订单簿里按“价格优先、时间优先”找能成交的对手单,算出一批
TradeExecution。 - 结算模块收到这批成交后,在一个数据库事务里同时完成:
- 买家扣冻结并扣余额、卖家加余额(钱包变更)
- 卖家扣库存、买家加库存(库存变更)
- 写入
trade_history(成交流水) - 更新订单
filled_quantity与状态(OPEN/MATCHING/PARTIALLY_FILLED/FILLED)
- 事务提交后,广播一次 SSE
refresh,前端收到就重新拉盘口/余额/成交列表。
这样做的直觉好处:
- 快:撮合在内存队列里完成,不用每次都扫数据库。
- 稳:真正“改钱/改库存”只发生在结算事务里,要么全成功,要么全回滚。
- 不乱:预占字段把资源锁住,避免一份钱/一份库存被重复使用。
撮合入口:backend/src/main/java/com/gamemarket/matching/MatchingEngineService.java
- 维度:
booksByAssetId,每个 assetId 一个 OrderBook。 - 并发:
OrderBook内部用 每资产一把锁(ReentrantLock)保证“撮合 + 入簿”原子性。 - 排序规则(价格优先,时间优先):
- Bid(买盘):价格 DESC,时间 ASC
- Ask(卖盘):价格 ASC,时间 ASC
- 自成交保护:如果 maker 和 taker 是同一玩家,会跳过该挂单继续找下一条(不会直接取消 taker)。
- 定价规则:maker-taker 定价,成交价使用 maker 订单价:
tradePrice = maker.price。
核心逻辑在:backend/src/main/java/com/gamemarket/matching/OrderBook.java#matchAndAdd
撮合的输出不会直接改数据库,而是生成 TradeExecutedEvent,并通过 publishAfterCommit 在事务提交后再发布事件,避免“订单没写入 DB,但撮合/结算先跑了”的竞态。
系统重启后的正确性:撮合引擎在启动时会从数据库恢复活跃订单到内存订单簿:
MatchingEngineService#recoverActiveOrders会加载OPEN/MATCHING/PARTIALLY_FILLED的订单并loadRestingOrder。
结算处理器:backend/src/main/java/com/gamemarket/matching/TradeExecutionHandler.java
- 触发方式:监听
TradeExecutedEvent(@EventListener) - 执行模型:
@Async异步执行,但方法本身是@Transactional,确保这批 execution 的结算在一个事务中完成 - 失败策略:任何异常都会
setRollbackOnly(),保证钱包/库存/成交记录不会“只写一半”
结算做的事情(按执行顺序):
- 处理被撮合引擎判定取消的订单(本实现预留了 cancelledOrderIds 的处理路径)
- 处理 executions(逐笔):
- 钱包转账:
- 买家:
WalletService.commitReserved(buyer, total)(reserved↓ + balance↓) - 卖家:
WalletService.addFunds(seller, total)(balance↑)
- 买家:
- 买单的
MarketOrder.reservedFunds也会随成交递减,用于支持“价格改善”后把剩余预占释放 - 库存转移:
- 卖家:
reservedQuantity↓,quantity↓ - 买家:若无该物品则创建
PlayerAsset,并quantity↑
- 卖家:
- 写入成交记录:
TradeHistory(asset、price、quantity、tradeTime、buyOrderId、sellOrderId) - 生成通知:写入
Notification(best-effort,不影响主交易事务)
- 钱包转账:
- 更新订单状态与 filledQuantity:
- OPEN → MATCHING(第一次成交时)
- filledQuantity 累加后:
- 未满:PARTIALLY_FILLED
- 满额:FILLED,并释放 BUY 订单剩余
reservedFunds(价格改善/未用完预占)
- 结算事务完成后,会广播
refresh:RealtimeEventService.broadcastRefresh()
这部分是交易一致性的核心:把“资金/库存/成交记录/订单状态”集中在同一个事务里更新。
入口:backend/src/main/java/com/gamemarket/service/OrderService.java#executeTrade
前端点击盘口“买入/卖出”时,会把 maker 的 orderId、自己的 userId、成交数量 quantity 提交给:
POST /api/market/trade→MarketController#executeTrade→OrderService#executeTrade
后端会:
- 对 maker 订单加行级锁(
findByIdForUpdate),校验状态仍可成交(OPEN/MATCHING/PARTIALLY_FILLED) - 校验不能与自己成交、数量合法
- 根据 maker 订单方向推导 taker 方向:maker=SELL → taker=BUY;maker=BUY → taker=SELL
- 先对 taker 做资源预占(资金 or 库存),然后创建一张 taker
MarketOrder(OPEN) - 将 taker 订单提交给撮合引擎:
matchingEngineService.submit(taker)
这样做的好处是:不需要写两套撮合逻辑,吃单/挂单都走同一套 OrderBook 撮合与 TradeExecutionHandler 结算。
撤单入口:backend/src/main/java/com/gamemarket/service/OrderService.java#beginCancel / cancelOrder / abortCancel
beginCancel:- 先对订单加行级锁
- 先从内存订单簿移除(
matchingEngineService.removeFromBook(order))避免继续被撮合 - 状态置为
CANCEL_PENDING
cancelOrder:- 释放剩余预占:BUY 释放
Wallet.reserved;SELL 释放PlayerAsset.reservedQuantity - 状态置为
CANCELLED,并写一条Notification
- 释放剩余预占:BUY 释放
abortCancel:- 把订单从
CANCEL_PENDING拉回OPEN - 重新
submit回订单簿
- 把订单从
后端使用 Server-Sent Events(SSE):
- 订阅接口:
GET /api/realtime/stream(backend/src/main/java/com/gamemarket/controller/RealtimeController.java) - 广播实现:
backend/src/main/java/com/gamemarket/realtime/RealtimeEventService.javabroadcastRefresh()会发出名为refresh的 SSE 事件
前端订阅在:src/utils/realtime.js,收到 refresh 后会派发 window 事件(realtime:refresh),各页面按需刷新:盘口、余额、成交历史等。
market_order(MarketOrder):order_type(BUY/SELL)price、quantity、filled_quantitystatus(CREATED/OPEN/MATCHING/PARTIALLY_FILLED/FILLED/CANCEL_PENDING/CANCELLED)reserved_funds(买单预占资金的“剩余可释放值”)
wallet:balance+reserved(预占资金不允许被重复下单使用)player_asset:quantity+reserved_quantity(预占库存不允许被重复挂卖)trade_history:成交明细(用于 K 线/成交列表/涨跌幅等统计)
- 下单/撤单/吃单入口:
backend/src/main/java/com/gamemarket/service/OrderService.javabackend/src/main/java/com/gamemarket/controller/TradeController.javabackend/src/main/java/com/gamemarket/controller/MarketController.java
- 撮合:
backend/src/main/java/com/gamemarket/matching/MatchingEngineService.javabackend/src/main/java/com/gamemarket/matching/OrderBook.java
- 结算:
backend/src/main/java/com/gamemarket/matching/TradeExecutionHandler.java
- 实时推送:
backend/src/main/java/com/gamemarket/realtime/RealtimeEventService.javasrc/utils/realtime.js
- 资产背包: 可视化管理持有的游戏饰品,支持分页浏览、查看稀有度、参考价和持仓成本。
- 钱包系统: 支持模拟充值、提现,并提供详细的资金流水记录。
- 订单管理: 实时查看当前挂单状态,支持随时撤单。
- 个人设置: 支持修改昵称、上传头像等个性化设置。
- Frontend:
- Vue 3 (Composition API)
- Vite (Build Tool)
- Vue Router (Routing)
- Pinia (State Management)
- ECharts (Data Visualization)
- CSS3 Animations & Canvas
- Backend:
- Java 21
- Spring Boot 3.3.5
- Spring Data JPA / Hibernate
- Lombok
- Database:
- PostgreSQL 16
- DevOps:
- Docker & Docker Compose
- Maven
- NPM
确保本地已安装:
- Node.js 18+
- Java 21 (JDK)
- Maven 3.8+
- Docker (可选,用于运行数据库)
项目根目录下运行:
docker start market-postgres
# 或者使用 docker-compose up -dcd backend
mvn spring-boot:run后端服务将运行在 http://localhost:8080。
npm install
npm run dev前端服务将运行在 http://localhost:5173。
├── backend/ # Spring Boot 后端源码
│ ├── src/main/java # Java 业务逻辑
│ └── uploads/ # 用户上传文件存储目录
├── database/ # SQL 初始化脚本
├── src/ # Vue 3 前端源码
│ ├── api/ # Axios 请求封装
│ ├── assets/ # 静态资源 & 全局样式
│ ├── components/ # 公共组件 (ParticleBackground, NavBar等)
│ ├── router/ # 路由配置
│ ├── store/ # Pinia 状态管理
│ └── views/ # 页面视图 (Market, Inventory, Profile等)
└── vite.config.js # Vite 配置 (含代理设置)
- 2025-11-29:
- 实现了基于 Canvas 的动态粒子背景。
- 修复了库存页面的分页显示问题。
- 完善了头像上传功能的本地存储与静态资源映射。
- 优化了市场筛选器的交互逻辑与加载状态。
Project maintained by SwissRo1l