ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SSM股票交易系统源码实战:环境搭建、核心表设计与交易撮合避坑指南

SSM股票交易系统源码实战:环境搭建、核心表设计与交易撮合避坑指南

简介:这是一套面向Java毕业设计与课程设计场景的SSM股票交易管理系统完整源码,适合具备Java Web基础、需要完成毕设或实训项目的学生与开发者。系统分为前后台:前台支持用户注册登录、广告轮播、股票推荐、金融实时、在线交流,以及股票收藏、评论、资金账户管理和买卖股票记录;后台提供用户、资金、买卖股票、股票板块与系统管理等权限,并支持发布股票与金融实时信息。压缩包共1268个文件,约19.8MB,以js、png、css、gif、java、jsp等为主,涵盖前端静态资源、页面模板与后端业务代码,另含sql脚本、xml配置及properties文件,便于直接导入运行。开发环境为JDK1.8、SSM+MyBatis、MySQL5.7及以上、Maven3.3及以上与Tomcat7及以上,兼容Eclipse或IDEA。目前已有62人学习,可作为完整赛题方案参考,帮助梳理分层结构、数据库设计与前后台交互逻辑。

1. 拿到一份 SSM 股票交易管理系统源码,先别急着跑

很多同学拿到「基于 SSM 的股票交易管理系统源码」这个压缩包,第一反应是解压、导入 IDE、点运行,然后被一堆报错劝退。我带过几届毕业设计,见过太多人卡在第一步:数据库连不上、Maven 依赖下不动、Tomcat 起不来,最后干脆换题。其实这套技术栈的骨架非常固定——Spring 管 Bean 和事务、SpringMVC 管请求分发、MyBatis 管 SQL 映射,业务上无非是用户、账户、股票、持仓、委托、成交这几张核心表。真正决定你能不能跑通的,不是代码写得多花哨,而是环境版本对不对、数据库脚本有没有按顺序执行、配置文件里的路径和账号有没有改成你自己的。

这篇文章面向三类人:正在做 Java 毕业设计、需要一套能跑通并讲清楚的项目;想拿 SSM 项目练手、补全 CRUD 到业务逻辑链路的新手;以及需要快速评估这套源码值不值得改、能不能作为二次开发底座的开发者。我会按「环境怎么搭 → 核心表怎么设计 → 交易逻辑怎么写 → 怎么避坑 → 怎么验证和扩展」的顺序讲,每一步都给出可复现的命令和参数。看完你应该能独立把项目跑起来,并且知道哪些地方是答辩时最容易被追问的。

2. 环境搭建:JDK、Maven、Tomcat 与 MySQL 的版本对齐

2.1 为什么版本选错比代码写错更致命

SSM 项目对版本极其敏感。Spring 4.x 和 Spring 5.x 的包名有变化,MyBatis 3.4 和 3.5 对@MapperScan的支持不同,JDK 8 和 JDK 11 在反射和模块化上的差异会让某些老依赖直接抛NoClassDefFoundError。我一般建议毕业设计统一用这套组合:JDK 8、Maven 3.6.x、Tomcat 8.5、MySQL 5.7、Spring 5.2.x、MyBatis 3.5.x。原因很简单——网上绝大多数 SSM 教程和现成源码都基于这个组合,遇到问题能搜到答案。如果你用 JDK 17 配 Spring 4,光是javax到jakarta的迁移就能耗掉两天。

先确认本机环境,逐条执行:

java -version mvn -v mysql --version

输出里 JDK 必须是 1.8.x,Maven 建议 3.6.3,MySQL 5.7.x。如果 JDK 是 11 以上,去装一个 JDK 8 并在 IDE 里单独指定,不要改系统全局变量,避免影响其他项目。

2.2 数据库初始化:建库、导脚本、改连接

拿到源码后先找sql目录或db目录,通常有一个stock.sql或init.sql。执行顺序不能乱,因为外键依赖决定了建表先后。常见做法是先建库再导数据:

CREATE DATABASE stock_trade DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE stock_trade; SOURCE /path/to/stock.sql;

导入完成后用SHOW TABLES;确认表数量,一般会有user、account、stock、position、order、trade、stock_price这几张。如果脚本里没有CREATE DATABASE语句,必须手动建库,否则导入会报Unknown database。

接着改jdbc.properties或applicationContext.xml里的连接信息:

jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/stock_trade?useUnicode=true&characterEncoding=utf8&useSSL=false jdbc.username=root jdbc.password=你的密码

注意 MySQL 5.7 用com.mysql.jdbc.Driver,MySQL 8 用com.mysql.cj.jdbc.Driver并且 URL 要加serverTimezone=Asia/Shanghai。这两个驱动类名写错,启动时一定报Cannot load driver class。

2.3 Maven 依赖拉取与 Tomcat 配置

在项目根目录执行mvn clean install -DskipTests。如果卡在下载,换阿里云镜像:

<mirror> <id>aliyun</id> <mirrorOf>central</mirrorOf> <url>https://maven.aliyun.com/repository/public</url> </mirror>

依赖拉完后,在 IDEA 里配置 Tomcat:Deployment 选war exploded,Application context 设成/stock,端口默认 8080。启动前确认web.xml里的contextConfigLocation指向的 Spring 配置文件路径真实存在。我见过有人把classpath:spring/applicationContext.xml写成classpath:applicationContext.xml,结果 Spring 容器根本没加载,所有 Bean 都是 null。

3. 核心表设计与 MyBatis 映射:从股票到持仓的数据链路

3.1 六张核心表的字段与关系

股票交易系统的数据模型比普通 CRUD 复杂,因为它有「账户余额 → 委托下单 → 成交 → 持仓变化」这条链路。下面是我整理的最小可用表结构,字段名和类型可以直接抄:

表名关键字段说明
userid, username, password, phone, status用户登录与状态
accountid, user_id, balance, frozen_amount资金账户,frozen 是挂单冻结
stockid, code, name, market, status股票基础信息
stock_priceid, stock_id, price, change_rate, trade_date行情快照
orderid, user_id, stock_id, type, price, amount, status委托单,type 买/卖
positionid, user_id, stock_id, amount, cost_price持仓,成本价用于算盈亏
tradeid, order_id, price, amount, trade_time成交记录

account和position是业务核心。下单时先冻结资金或持仓,成交后再扣减,这个「冻结—扣减」两步是答辩必问点。很多源码为了省事直接一步扣款,遇到并发就会超卖。

3.2 MyBatis 映射文件与动态 SQL

以OrderMapper.xml为例,查询当前用户的委托列表需要按状态和股票代码过滤,用动态 SQL:

<select id="selectByUser" resultMap="OrderResultMap"> SELECT o.*, s.name AS stock_name, s.code AS stock_code FROM `order` o LEFT JOIN stock s ON o.stock_id = s.id WHERE o.user_id = #{userId} <if test="status != null"> AND o.status = #{status} </if> <if test="stockCode != null and stockCode != ''"> AND s.code = #{stockCode} </if> ORDER BY o.create_time DESC </select>

resultMap里要把stock_name和stock_code映射到 VO 的属性上,否则前端拿不到股票名称。参数status用 Integer,stockCode用 String,<if>里的判空要同时判断 null 和空串,只写!= null在传空字符串时会拼出AND s.code = '',查不到数据。

3.3 Service 层事务与冻结逻辑

下单接口必须加@Transactional,并且把冻结和插入委托放在同一个事务里:

@Service public class OrderServiceImpl implements OrderService { @Autowired private AccountMapper accountMapper; @Autowired private OrderMapper orderMapper; @Override @Transactional(rollbackFor = Exception.class) public void buy(Long userId, Long stockId, BigDecimal price, Integer amount) { BigDecimal total = price.multiply(new BigDecimal(amount)); // 冻结资金,update 带条件防止余额不足 int rows = accountMapper.freeze(userId, total); if (rows == 0) { throw new BizException("余额不足"); } Order order = new Order(); order.setUserId(userId); order.setStockId(stockId); order.setType(1); order.setPrice(price); order.setAmount(amount); order.setStatus(0); orderMapper.insert(order); } }

freeze的 SQL 要写成UPDATE account SET balance = balance - #{total}, frozen_amount = frozen_amount + #{total} WHERE user_id = #{userId} AND balance >= #{total}。WHERE后面的balance >= #{total}是关键,它利用数据库行锁保证并发下不会扣成负数。rollbackFor = Exception.class确保任何异常都回滚,默认只回滚 RuntimeException,受检异常不会触发回滚,这是血泪经验。

4. 交易撮合与行情模拟:让系统真正「动」起来

4.1 撮合逻辑的最小实现

毕业设计不需要真的对接交易所,常见做法是写一个定时任务或手动触发的撮合方法:遍历状态为「已报」的委托,拿当前行情价和委托价比较,买单委托价大于等于市价就成交,卖单委托价小于等于市价就成交。成交后更新委托状态、写入trade表、调整持仓和账户。

@Scheduled(fixedDelay = 5000) public void match() { List<Order> pending = orderMapper.selectByStatus(0); for (Order order : pending) { StockPrice price = priceMapper.selectLatest(order.getStockId()); if (price == null) continue; boolean canMatch = order.getType() == 1 ? order.getPrice().compareTo(price.getPrice()) >= 0 : order.getPrice().compareTo(price.getPrice()) <= 0; if (canMatch) { tradeService.doSettle(order, price.getPrice()); } } }

fixedDelay = 5000表示上一轮结束后隔 5 秒再跑,避免任务堆积。doSettle里要处理持仓:买单增加持仓并重算成本价,卖单减少持仓并解冻资金。成本价公式是(原持仓成本 * 原数量 + 成交价 * 成交数量) / (原数量 + 成交数量),这个公式答辩时经常被要求现场推导。

4.2 行情数据从哪来

没有实时行情源,就用模拟数据。在stock_price表里预置一批历史价格,然后写一个随机波动方法:

public BigDecimal nextPrice(BigDecimal current) { double rate = (Math.random() - 0.5) * 0.04; BigDecimal next = current.multiply(BigDecimal.valueOf(1 + rate)); return next.setScale(2, RoundingMode.HALF_UP); }

波动幅度控制在 ±2% 以内,否则价格会很快跑到不合理区间。setScale(2)保留两位小数,和 A 股报价一致。每次生成新价格时插入一条stock_price记录,撮合任务取最新一条。这样系统就有了「行情变化 → 委托成交 → 持仓盈亏」的完整闭环,演示时效果比静态页面好得多。

4.3 盈亏计算与前端展示

持仓页面的浮动盈亏 =(最新价 - 成本价) * 持仓数量。这个计算放在 Service 层,不要在前端算,因为前端拿不到实时最新价。返回给前端的 VO 里带上currentPrice、costPrice、profit、profitRate四个字段。profitRate用profit / (costPrice * amount)计算,注意除零保护。

前端用 JSP 或 Thymeleaf 都行,SSM 项目多数用 JSP。表格里用<c:forEach>遍历持仓列表,盈亏为正显示红色,为负显示绿色,符合国内用户习惯。分页可以用 PageHelper,在mybatis-config.xml里配好插件,Service 里PageHelper.startPage(pageNum, pageSize)即可。

5. 避坑与排查:启动失败、乱码、事务失效的 5 个真实场景

5.1 启动报 NoClassDefFoundError 或 ClassNotFoundException

现象:Tomcat 启动时控制台刷java.lang.NoClassDefFoundError: org/springframework/context/ApplicationContext。原因通常是 Maven 依赖没有打进WEB-INF/lib,或者 IDEA 的 Artifact 配置里漏了库。解决:在 Project Structure → Artifacts 里确认WEB-INF/lib下包含所有 Spring 和 MyBatis 的 jar;或者执行mvn dependency:copy-dependencies手动拷贝。另一个常见原因是spring-webmvc和spring-context版本不一致,统一改成同一版本号。

5.2 数据库中文乱码

现象:股票名称显示成???或大盘。原因有三层:数据库字符集不是 utf8mb4、连接 URL 没加characterEncoding=utf8、JSP 页面没声明pageEncoding="UTF-8"。解决:建库时指定utf8mb4,URL 加useUnicode=true&characterEncoding=utf8,JSP 头部加<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>。三层都改完还乱码,检查 MySQL 的my.ini里character-set-server是不是 utf8。

5.3 @Transactional 不生效

现象:下单时余额扣了,但插入委托失败后余额没回滚。原因通常是同类内部方法调用,比如buy()里直接调this.freeze(),不走代理对象,事务注解失效。解决:把冻结逻辑抽到另一个 Service,或者注入自身代理。另一个原因是方法不是public,Spring AOP 对非 public 方法不生效。还有一点,异常被try-catch吞掉没抛出,事务也不会回滚。

5.4 静态资源 404

现象:页面能打开但 CSS、JS、图片全部 404。原因是 SpringMVC 的DispatcherServlet拦截了所有请求,包括静态资源。解决:在spring-mvc.xml里加<mvc:default-servlet-handler/>和<mvc:resources mapping="/static/**" location="/static/"/>,同时确保DispatcherServlet的url-pattern是/而不是/*。/*会连 JSP 一起拦截,导致视图解析失败。

5.5 定时任务重复执行

现象:撮合任务每 5 秒跑一次,但日志显示同一秒执行了两次。原因是 Spring 配置文件被加载了两次,或者 Tomcat 部署了两个 context。解决:检查web.xml里ContextLoaderListener和DispatcherServlet是否都加载了包含<task:annotation-driven/>的配置文件,重复加载会导致定时器注册两次。把定时任务配置单独放一个文件,只让ContextLoaderListener加载。

6. 从能跑到能讲:二次开发与答辩验证的实用技巧

项目跑起来只是及格线,答辩时老师不会只看页面,他会问「你这个冻结资金怎么保证不超卖」「并发下订单状态怎么控制」「如果行情源断了系统会怎样」。所以最后一章我讲几个能让你从「能跑」变成「能讲」的实操技巧。

第一个技巧是给关键接口加日志埋点。在OrderServiceImpl的buy和doSettle方法入口用log.info打印 userId、stockId、amount 和当前余额,出问题时能快速定位是冻结失败还是插入失败。日志用 SLF4J + Logback,在logback.xml里把com.yourpackage.mapper的级别设成 DEBUG,能看到实际执行的 SQL 和参数,比盲猜高效得多。

第二个技巧是写一个并发测试用例验证冻结逻辑。用CountDownLatch模拟 10 个线程同时下单,每个线程买 100 股,账户余额只够 5 笔成交。跑完检查balance和frozen_amount之和是否等于初始余额,order表里成功插入的条数是否等于 5。这个测试能直接证明你的WHERE balance >= #{total}起了作用,答辩时把测试结果一摆,比说一百句「我加了事务」都有说服力。

@Test public void testConcurrentBuy() throws Exception { int threads = 10; CountDownLatch latch = new CountDownLatch(threads); ExecutorService pool = Executors.newFixedThreadPool(threads); for (int i = 0; i < threads; i++) { pool.submit(() -> { try { orderService.buy(1L, 1L, new BigDecimal("10.00"), 100); } catch (Exception e) { // 余额不足的异常在这里被捕获 } finally { latch.countDown(); } }); } latch.await(); Account acc = accountMapper.selectByUserId(1L); assertEquals(0, acc.getBalance().compareTo(new BigDecimal("50.00"))); }

第三个技巧是准备一份「数据流讲解稿」。从用户点击买入按钮开始,请求经过DispatcherServlet→OrderController→OrderService→AccountMapper.freeze→OrderMapper.insert,返回后前端刷新持仓。把这条链路画在纸上,标出事务边界和行锁位置,答辩时照着讲,老师能看出你真的理解代码而不是抄的。我一般还会准备一个「如果要把 MySQL 换成 Redis 做缓存,哪些地方要改」的扩展问题,答案是把stock_price的最新价放 Redis,撮合任务从 Redis 读,减少数据库压力。

最后一个习惯:每次改完代码,先mvn clean再启动。SSM 项目编译产物有时候会残留旧 class,导致改了代码没生效,这种玄学问题我踩过不止一次。把clean和install绑成一个 Maven 命令,启动前跑一遍,能省掉大量「为什么改了没用」的排查时间。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进