news 2026/9/16 1:39:15

SSM股票交易管理系统开发实战:Spring MVC+MyBatis事务与并发控制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SSM股票交易管理系统开发实战:Spring MVC+MyBatis事务与并发控制

简介:面向Java毕业设计与课程设计场景,这套股票交易管理系统基于SSM框架(Spring+SpringMVC+MyBatis)与MySQL数据库,完整实现了前台用户注册登录、股票信息浏览与推荐、收藏评论、个人资金账户管理以及买入卖出操作,后台则涵盖用户管理、资金管理、股票与板块管理、交易记录管理等核心模块,功能链路完整,适合需要快速搭建可演示项目的学生参考。压缩包共收录1268个文件,整体大小约19.72MB,包含120个Java后端类、105个JSP页面、364个JavaScript脚本、146个CSS样式,以及数量丰富的PNG/JPG/GIF图片素材、XML配置、SQL数据库脚本和说明文档,目录划分清晰,便于按模块导入Eclipse/IDEA学习。目前已有137人学习下载。随包附带说明文档与LW文档,能够帮助理解数据库表结构、业务逻辑和前后台交互流程;本地部署只需JDK1.8、MySQL5.7、Tomcat7及以上环境,可作为毕业设计答辩或课程实践的项目蓝本。

1. 股票交易管理系统:从 SSM 骨架到可复用的交易闭环

我见过不少同学把股票交易系统放进 Java 课程设计选题时,第一反应是:这里是不是要去调券商行情接口?其实不用。这套基于 SSM(Spring、Spring MVC、MyBatis)和 MySQL 5.7 的源码把后台和前台全部写在同一个 Maven 工程里,前台解决用户注册、股票浏览、金融资讯、在线交流,后台解决用户管理、股票板块、买入卖出和账户资金管理,属于典型的后台管理系统加一个交易前台。它真正值得复盘的,是资金扣减、订单记录和 MyBatis 事务这些日常工作里躲不掉的环节。适合正在做 Java 毕业设计、课程设计,或者想把 SSM 框架在真实场景里完整串一遍的开发者。

2. SSM 分层、账户表设计:先立住交易主链路的数据模型

2.1 为什么这套系统用 SSM 而不是 Spring Boot

这个项目没有选择 Spring Boot,而是 SSM,核心原因是它的运行环境是 Tomcat 7 和 JDK 1.8,很多学校机房和老项目还停留在 JSP + Servlet 的工作方式。Spring 负责管理 Service 和 DAO 的生命周期,Spring MVC 的 DispatcherServlet 接收/stock/list这类请求,MyBatis 则把 SQL 结果映射到实体类,避免手写一堆 ResultSet 转换。三层边界一旦清楚,后面增加接口或改数据库字段时,不会牵一发动全身。

对比直接用 Servlet 写业务,SSM 的优势在于 SQL 被隔离在 Mapper 文件里,Java 代码只需要关心对象和方法;页面渲染交给 JSP,Controller 只做参数接收、调用和转发。股票列表页、个人中心页和后台管理页共用同一套数据模型,不需要拆两套后台,这对只有一两个人维护的课程设计来说很重要。

2.2 核心数据表:用户、资金账户、股票和交易流水

按照功能描述,我把核心数据表梳理成下面这张关系表。实际你拿到源码后再对照字段,业务含义不会跑偏。

表名业务含义关键字段
user前台用户id, id_card, password, name, phone, area, create_time
account资金账户id, user_id, balance, frozen_amount, update_time
stock_block股票板块id, block_name, description
stock股票标的id, stock_code, stock_name, block_id, current_price, update_time
trade_record交易流水id, user_id, stock_id, trade_type, order_price, shares, amount, trade_time
stock_favorite股票收藏id, user_id, stock_id, create_time
stock_comment在线交流/评论id, user_id, stock_id, content, create_time

这里把买入和卖出都放进 trade_record,用 trade_type=1 表示买入,trade_type=2 表示卖出。我倾向于合并成一张表,而不是分成 buy_record 和 sell_record,因为个人中心展示“最近交易”时只需要一次分页查询,统计持仓盈亏时也少两次 join。账户表与用户表是一对一关系,注册成功后必须同时插入用户和资金账户,否则前台登录后查不到余额,这一步骤在后面的事务里会看到写法。

股票表通过 block_id 关联板块表,后台的“股票管理”和“股票板块管理”维护的就是这两张表。数据库脚本请用 InnoDB 引擎,后续的 FOR UPDATE 行锁和事务回滚都依赖这个引擎。

2.3 搭建 Maven 工程并把 MyBatis 数据源接好

工程结构按经验拆成 controller、service、dao、entity、interceptor 五层,Model 层用简单 JavaBean。resources 里放四个 Spring 配置文件和一个 mapper 目录。做法如下。

stock-trading/ ├── pom.xml ├── src/main/java/com/stock │ ├── controller │ ├── service │ ├── dao │ ├── entity │ └── interceptor ├── src/main/resources │ ├── jdbc.properties │ ├── spring-dao.xml │ ├── spring-service.xml │ ├── spring-mvc.xml │ ├── mybatis-config.xml │ └── mapper └── src/main/webapp ├── static └── WEB-INF/jsp

spring-dao.xml 里把数据源、SqlSessionFactory、Mapper 扫描器一次性配好。下面是这套源码最常见的配置写法。

<!-- 读取 jdbc.properties 中的数据库连接参数 --> <context:property-placeholder location="classpath:jdbc.properties"/> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="driverClass" value="${jdbc.driver}"/> <property name="jdbcUrl" value="${jdbc.url}"/> <property name="user" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> <property name="maxPoolSize" value="20"/> <property name="minPoolSize" value="5"/> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> <property name="typeAliasesPackage" value="com.stock.entity"/> <property name="configLocation" value="classpath:mybatis-config.xml"/> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.stock.dao"/> </bean>

jdbc.properties 里有一项必须注意:URL 要写成jdbc:mysql://localhost:3306/stock?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai。MySQL 5.7 本身没问题,但 JDK 1.8 配新版 MySQL Connector/J 时,不写 serverTimezone 会在启动时报时区错误。maxPoolSize 调成 20 对毕业设计足够,如果代码里出现Connection is not available,先看是不是连接池太小,而不是马上改拆库。

3. 注册登录与股票信息流:从参数绑定到 MyBatis 查询

3.1 注册查重和资金账户同时创建

前台注册时,用户提交身份证号、姓名、手机号、地区作为基本资料,密码字段至少做一次摘要存储。注册接口的核心校验是先按身份证查用户是否存在,存在则直接抛出业务异常,不存在才插入。注意业务异常要在 Service 里抛出,而不是在 Controller 里写 if/else,这样事务边界才清晰。

@Service public class UserServiceImpl implements UserService { @Autowired private UserDao userDao; @Autowired private AccountDao accountDao; @Override @Transactional(rollbackFor = Exception.class) public boolean register(UserVO vo) { // 身份证唯一校验,防止重复注册 if (userDao.selectByIdCard(vo.getIdCard()) != null) { throw new BusinessException("该身份证号已注册"); } User user = new User(); user.setIdCard(vo.getIdCard()); user.setName(vo.getName()); user.setPhone(vo.getPhone()); user.setArea(vo.getArea()); user.setPassword(DigestUtils.md5DigestAsHex( (vo.getPassword() + SALT).getBytes())); userDao.insert(user); // 初始化资金账户,默认给 10 万虚拟资金 Account account = new Account(); account.setUserId(user.getId()); account.setBalance(BigDecimal.valueOf(100000)); accountDao.insert(account); return true; } }

逻辑说明:先查身份证是否重复,再写入用户,最后初始化资金账户。@Transactional(rollbackFor = Exception.class)保证用户插入成功但账户插入失败时,两个 insert 都回滚,不会出现“用户存在却没有资金账户”的脏数据。很多人只写@Transactional,遇到检查型异常时事务不会回滚,这是一个高频面试点。SALT 是自定义常量,密码存储不要用明文,MD5 加盐虽然不算强,但在课程设计环境里足够;如果放到真实环境,建议换 BCrypt。

3.2 股票列表:多条件查询与 PageHelper 分页

前台的股票列表页需要支持按股票名称、股票代码、板块分类过滤。Controller 接收三个参数:keyword、blockId、pageNum。分页采用 PageHelper,因为它能在 MyBatis 执行器层自动改写 SQL,比分页插件自己拼 LIMIT 更稳。

@RequestMapping("/stock/list") public String list(String keyword, Integer blockId, @RequestParam(defaultValue = "1") Integer pageNum, Model model) { // startPage 只对下一条 SELECT 生效 PageHelper.startPage(pageNum, 10); List<Stock> stockList = stockDao.searchStock(keyword, blockId); PageInfo<Stock> pageInfo = new PageInfo<>(stockList); model.addAttribute("pageInfo", pageInfo); return "stock/list"; }

对应的 Mapper XML 是动态 SQL 的标准写法。

<!-- 股票关键字与板块联合查询 --> <select id="searchStock" resultType="com.stock.entity.Stock"> SELECT * FROM stock <where> <if test="keyword != null and keyword != ''"> AND (stock_name LIKE CONCAT('%', #{keyword}, '%') OR stock_code LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="blockId != null"> AND block_id = #{blockId} </if> </where> ORDER BY id DESC </select>

参数说明:keyword 为 null 时整个条件块被跳过,blockId 同理。CONCAT('%', #{keyword}, '%')是参数化拼接,不能只写'%#{keyword}%',后者会被 MySQL 当作字符串常量处理,查不出来数据。另外,PageHelper 会在 startPage 后的第一条查询上加 limit,所以 stockDao.searchStock 内部不要有多条 SELECT,否则分页信息会被后执行的 SQL 覆盖。如果你不想依赖 PageHelper,也可以直接用 MySQL 的 LIMIT #{offset}, #{pageSize},但需要手动计算 offset,方案简单但对前端不友好。

3.3 收藏和评论:个人中心的聚合数据

导航栏里的“股票”和“股票资讯”展示内容,用户可以对单个股票做收藏和评论。收藏接口建议写成切换式,即存在则取消,不存在则新增,避免前端连续点击产生重复记录。

@PostMapping("/favorite/toggle") @ResponseBody public Result toggle(@RequestParam Integer userId, @RequestParam Integer stockId) { // 存在则取消,不存在则新增,保持接口幂等 int exists = favoriteDao.checkExists(userId, stockId); if (exists > 0) { favoriteDao.delete(userId, stockId); return Result.ok("取消收藏"); } Favorite favorite = new Favorite(); favorite.setUserId(userId); favorite.setStockId(stockId); favoriteDao.insert(favorite); return Result.ok("收藏成功"); }

这里的关键点是 checkExists 的 SQL 一定要写成SELECT COUNT(1)而不是SELECT *,这样 MySQL 不需要回表拿整行数据,查询成本低。个人中心里的“我的收藏”“我的评论”就是三张表按用户 ID 过滤后分页。评论内容注意在 JSP 里使用<c:out>输出,防止用户输入脚本内容被浏览器解析。这个不是 SSM 特有的问题,但交易系统的在线交流模块正好容易忽略。

4. 买入卖出、账户余额与事务并发控制

4.1 交易接口里的业务规则

买入和卖出是这套系统真正有含金量的地方。我用一条买入接口来说明完整链路:校验股票存在、校验数量为正整数、计算金额、校验余额、扣余额、写入交易流水、更新持仓。卖出与买入对称,它先校验持仓是否足够,再从持仓表扣减,最后把卖出金额加回余额。

有个经验是,项目里至少要维护一张user_stock_position持仓表,否则“个人中心-我的股票”没法直接展示当前持有数量。很多毕业设计只记录买卖流水,不更新持仓,做演示可以,到了答辩环节会被问到“用户今天买了 100 股,明天再卖 50 股,剩余持仓从哪里来”就卡住了。所以我一般会在建表时加上一张持仓表,字段为id, user_id, stock_id, shares, update_time

4.2 @Transactional + FOR UPDATE 锁住余额

下面这段代码是买入接口的核心事务逻辑,重点在 selectByUserIdForUpdate。

@Transactional(rollbackFor = Exception.class) public void buy(BuyRequest request) { // 锁住账户行,防止并发扣款超出余额 Account account = accountDao.selectByUserIdForUpdate(request.getUserId()); Stock stock = stockDao.selectByCode(request.getStockCode()); if (stock == null) { throw new BusinessException("股票不存在"); } // 金额用 BigDecimal 计算,避免 double 精度丢失 BigDecimal orderAmount = stock.getCurrentPrice() .multiply(BigDecimal.valueOf(request.getShares())); if (account.getBalance().compareTo(orderAmount) < 0) { throw new BusinessException("可用余额不足"); } account.setBalance(account.getBalance().subtract(orderAmount)); accountDao.updateBalance(account); // 写入交易流水,确保与扣款在同一事务中 TradeRecord record = new TradeRecord(); record.setUserId(account.getUserId()); record.setStockId(stock.getId()); record.setTradeType(1); record.setOrderPrice(stock.getCurrentPrice()); record.setShares(request.getShares()); record.setAmount(orderAmount); tradeRecordDao.insert(record); positionDao.increasePosition(account.getUserId(), stock.getId(), request.getShares()); }

selectByUserIdForUpdate 对应的 SQL 是SELECT * FROM account WHERE user_id = #{userId} FOR UPDATE。得到账户行后,MySQL 会把这行的写锁一直持有到事务提交。这样两个线程同时买入时,第二个线程会在 SELECT 处等待,不会出现两人同时读到余额 1000 元然后各自扣 800 元成功,导致余额变成 -600 的覆盖问题。

金额计算必须用 BigDecimal,不能用 double。股票现价 19.9 元、买 100 股,double 算出来可能会带浮点尾巴,存到 decimal(18,2) 虽然会被四舍五入,但日志和订单金额中间结果会莫名其妙多出 0.00001。这种误差在金融系统里非常要命。compareTo 比较余额时也避免写balance >= amount,BigDecimal 的 equals 还会比较精度,compareTo 才是真正的数值比较。

4.3 事务失效的三个隐蔽场景

这段代码如果运行后出现“余额被扣但流水没写入”或“余额没被扣但页面提示成功”,优先检查三个点。

第一个是自调用。同类中 buy() 直接调 this.buy(),Spring 事务代理不会拦截,注解失效。正确做法是通过注入的 self 或者把事务方法放到另一个 Service 中调用。

第二个是 try-catch 吞异常。业务里最常见的是:

try { accountDao.updateBalance(account); tradeDao.insert(record); } catch (Exception e) { return false; }

异常被捕获后,事务通知收到的不是异常而是正常返回,回滚不会触发。正确做法是把异常继续往外抛,由统一异常处理器转成 JSON 返回。

第三个是数据库引擎表用了 MyISAM。MyBatis 和 Spring 的事务注解只管应用层,底层表不支持事务时,回滚就是空话。建议建表时直接写ENGINE=InnoDB DEFAULT CHARSET=utf8mb4,另外注意 @Transactional 只对 Spring 代理的 Bean 生效,如果 Service 类没被 Spring 管理,注解就是摆设。

4.4 个人中心的交易流水查询

个人中心需要同时展示买入、卖出历史,并能在每条记录后面看到对应股票信息。用 trade_record 主表 left join stock 表实现,避免一次取出所有字段导致 JSP 页面重复查库。

<select id="selectPageByUserId" resultType="com.stock.dto.TradeRecordVO"> SELECT r.id, s.stock_code, s.stock_name, r.trade_type, r.order_price, r.shares, r.amount, r.trade_time FROM trade_record r LEFT JOIN stock s ON r.stock_id = s.id WHERE r.user_id = #{userId} ORDER BY r.id DESC </select>

trade_type 在页面显示为“买入”或“卖出”,金额格式保留两位小数。如果这里出现股票名称为空,先检查 stock 是否被后台删除,最好在删除股票时做逻辑删除而不是物理删除,否则历史订单的 join 结果会把整条记录丢给 java 的空指针。

5. Navicat 导入、Tomcat 时区与线上验证技巧

5.1 一条 SQL 验证资金和交易流水是否一致

交易功能做完后,不要只靠页面肉眼对比余额。用这条 SQL 直接把每个用户的账面余额和交易流水推导出的资金差拉出来对比。

-- 对比账面余额与流水推导余额,检查事务一致性 SELECT u.id, (SELECT SUM(CASE WHEN trade_type = 1 THEN -amount ELSE amount END) FROM trade_record t WHERE t.user_id = u.id) AS calc_balance, a.balance FROM user u JOIN account a ON a.user_id = u.id;

calc_balance 为负数说明流水累计买入金额大于卖出金额,最终账面余额应该等于初始资金加上这一个差额。如果不一致,重点检查买入/卖出金额是否重复记录,以及事务回滚是否把流水回滚了但余额没回滚。这条 SQL 也可以作为 mysql 面试题里关联子查询的延伸,养成输出数据而不是打印日志的习惯,排错效率会高很多。

5.2 部署前改掉的三处配置

用 Navicat 11 导入脚本前,先确认连接字符集是 utf8mb4。如果只查“股票”两个字没问题,但评论里插入 emoji 变成问号,大概率是库表用了 utf8,而 utf8 在 MySQL 中最大只有 3 字节,存不下 emoji。Tomcat 部署时,把 jdbc.url 的 serverTimezone 值改成 Asia/Shanghai,避免卖出时间比本地时间早 8 小时;pom.xml 里最好把 finalName 改成 stock.war,这样访问路径固定为 /stock。

配置项推荐值原因
jdbc.url 字符集characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai中文乱码与 JDBC 8 时区
Navicat 连接编码utf8mb4评论中 emoji 不落库
Spring CharacterEncodingFilterUTF-8POST 表单中文参数不乱码

5.3 日志里看到回滚再下结论

把 Spring 事务的日志级别放出来,排查余额问题时很快能定位到事务边界。

# 打开事务日志,观察回滚行为 log4j.logger.org.springframework.transaction=DEBUG

日志中出现 Transaction rolled back 时,说明注解、异常机制是生效的,问题在业务代码里;如果日志什么都没输出,但业务莫名其妙失败,先查 @Transactional 是否真的代理了 Service。把这块配置加好之后,再遇到“余额扣了但订单没生成”的情况,对着日志看就清楚是哪个环节抛出的异常,不用一行行打断点。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 1:38:21

CCD与CMOS图像传感器:原理、信号处理与调试全解析

做机器视觉这些年&#xff0c;被问得最多的问题就是&#xff1a;CCD和CMOS到底差在哪&#xff1f;为什么工业相机还在用CCD&#xff1f;手机传感器是CMOS为什么画质也那么好&#xff1f;还有不少人把图像传感器领域的CMOS&#xff0c;和电脑主板上那个保存BIOS设置的纽扣电池混…

作者头像 李华
网站建设 2026/9/16 1:38:19

SAP COPA客户字段派生实战:从KEA5定义到CMOD增强完整指南

做SAP FICO的&#xff0c;尤其是碰COPA&#xff08;获利能力分析&#xff09;的顾问&#xff0c;迟早会撞上一个需求&#xff1a;管理层要按某个系统里没有的维度看毛利。比如说&#xff0c;“客户渠道”&#xff0c;销售总监说我要看直营、分销、电商各赚多少钱&#xff0c;但…

作者头像 李华
网站建设 2026/9/16 1:37:46

CefSharp单页面应用:地址栏导航、网页加载控制与文件下载实现

简介&#xff1a;本资源是一套基于VB.NET开发的CefSharp单页面浏览器完整源码工程&#xff0c;面向Windows桌面应用开发者&#xff0c;解决在WinForm或WPF中嵌入Chromium内核并实现网页加载、地址栏导航与文件下载功能的核心需求。适用于需要定制轻量级浏览器界面、集成网页交互…

作者头像 李华
网站建设 2026/9/16 1:36:20

图数据挖掘核心指标:K-core、Truss、Clique与ECC实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 1:35:50

AI时代程序员生存指南:从写代码到定方向

"AI来了&#xff0c;程序员怎么办"这个问题&#xff0c;这两年几乎每次技术聚会都会被人拎出来问一遍。我用AI编程工具写了快两年的业务代码&#xff0c;带过用Copilot做主力开发的小团队&#xff0c;也面试过不少候选人&#xff0c;今天就把我看到的真实情况、踩过的…

作者头像 李华
网站建设 2026/9/16 1:35:16

XMC1300直流无刷电机驱动实战:换相、外设与调试要点

简介&#xff1a;这是基于英飞凌XMC1300单片机&#xff08;ARM Cortex-M0内核&#xff09;的直流无刷电机&#xff08;BLDC&#xff09;驱动控制工程包&#xff0c;面向嵌入式工程师、电子竞赛爱好者及电机控制初学者&#xff0c;用于解决XMC1300平台下BLDC驱动程序设计、编译与…

作者头像 李华