ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

SSM+MySQL股票交易系统:从数据库设计到事务实践

SSM+MySQL股票交易系统:从数据库设计到事务实践 简介这是一套基于SSM框架的Java股票交易管理系统毕业设计源码适用于计算机相关专业毕业设计、课程设计或实训项目开发。系统分为前台用户端与后台管理端前台提供注册登录、股票推荐、金融实时、股票资讯、收藏评论、个人资金账户及买入卖出操作后台涵盖用户管理、账户资金管理、股票与板块管理、买卖交易记录管理和系统管理每笔交易均会留存流水。开发环境基于JDK1.8、MySQL5.7及以上、Tomcat7及以上可使用eclipse或idea导入运行。资源包共1268个文件大小19.72MB主要类型包括java源码、jsp页面、js脚本、css样式、gif/jpg图片资源、xml配置、sql数据库脚本及说明文档doc、md等目录结构完整便于按模块查找和二次修改。当前已有137人浏览学习系统贴合股票交易业务场景能直观展示SSM整合开发流程、账户资金联动与交易记录设计对学习权限管理、数据表设计及SSM项目部署具有较高参考价值。1. 股票交易管理系统毕设到底在考核什么股票交易管理系统是 Java 毕业设计里出现频率极高的题目但它不是让你真接券商行情而是把「用户、股票、交易、持仓」四个核心对象用一套 Web 订单流程串起来。技术栈写的是 ssmmysql说明这套系统仍然用 SSM 骨架Spring 管对象、SpringMVC 管请求、MyBatis 管 SQL配 MySQL 做持久化。毕设考核点通常在三点第一数据库设计的完整性和事务边界是否清晰第二SSM 三层之间是否做到解耦Mapper、Service、Controller 各司其职第三说明文档和 LW 里是否能把 ER 图、用例图、测试结果讲圆。适合正在选毕设题目的学生也适合想快速把 SSM 捡起来的老开发——用一个小而完整的交易系统把框架整合流程重新过一遍。2. SSM 框架下股票交易系统的分层与数据模型设计2.1 为什么毕设选 SSM 而不是 Spring Boot很多人在选题时会犹豫现在企业都用 Spring Boot为什么还要用 SSM 做毕业设计原因很直接SSM 的配置是显式的能看见 Spring 容器如何加载、MyBatis 如何与数据源绑定、事务通知如何织入 Service 方法这些对理解框架原理帮助很大。Spring Boot 把配置自动化后很多同学只会在 Autowired 上写注解出了问题反而无从下手。股票交易管理系统用 SSM 还有一个好处MyBatis 的 XML 映射可以清晰看到买卖股票时那几条关键 SQL而不像 JPA 那样被自动生成语句掩盖。分层上我一般把项目分成四个包controller、service、mapper、pojo。controller 只做参数接收和视图转发service 写业务逻辑并把事务注解打在实现类上mapper 接口对应 XML 里的 SQLpojo 放实体类。这样分层的目的是让每一层的依赖方向都朝下controller 依赖 serviceservice 依赖 mapper谁都不跨层调。后续换数据库或者改查询逻辑只需要动最下面一层。2.2 数据库表结构用户、股票、持仓、交易流水表设计是整个系统的地基可以从四张核心表开始user 用户表、stock 股票表、position 持仓表、trade_record 交易流水表。再从这四张表派生出其他辅助表。需要注意的是股票价格不应该直接写在交易流水里就算完还要在 stock 表里维护一个当前行情价格供用户看盘时展示而持仓表里的成本价是通过历史交易流水计算得到的不能简单用一个字段存死。2.2.1 用户表和股票表的字段设计用户表不要复杂字段够用就行id、username、password、balance可用余额、create_time。其中 balance 用 DECIMAL(15,2)千万别用 FLOAT后面做金额累加时浮点误差会非常恶心。股票表至少要有id、stock_code股票代码、stock_name、current_price、update_time。股票代码用 VARCHAR因为像 600001 这样的代码如果存成数字前面的零会丢。SQL 建表语句如下CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, balance DECIMAL(15,2) NOT NULL DEFAULT 0.00, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE stock ( id INT NOT NULL AUTO_INCREMENT, stock_code VARCHAR(10) NOT NULL UNIQUE, stock_name VARCHAR(50) NOT NULL, current_price DECIMAL(10,2) NOT NULL, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里把 user 和 stock 的 id 都设为自增主键用 INT 足够应付毕设规模。password 字段建议存密文哪怕只是简单的 MD5也比明文强。股票表里的 update_time 加上 ON UPDATE CURRENT_TIMESTAMP可以让每次改价自动更新省一条 update 语句。2.2.2 持仓与交易流水的事务边界持仓表 position 结构为id、user_id、stock_id、quantity持仓数量、available_quantity可用数量、cost_price成本价。这里为什么要区分 quantity 和 available_quantity因为买入股票后当天不能卖出的情况下两个字段会不同相当于冻结了一部分份额。交易流水表 trade_record 为id、user_id、stock_id、trade_typeBUY/SELL、price、quantity、amount、trade_time。事务的边界在「下单」这个动作上。买入一支股票事务内要完成四件事检查用户余额是否足够、扣减余额、增加或更新持仓、插入一条 trade_record。卖出操作则是反向检查持仓数量是否够、扣减持仓、增加余额、插入 trade_record。任何一个环节失败整个事务回滚。CREATE TABLE position ( id INT NOT NULL AUTO_INCREMENT, user_id INT NOT NULL, stock_id INT NOT NULL, quantity INT NOT NULL DEFAULT 0, available_quantity INT NOT NULL DEFAULT 0, cost_price DECIMAL(10,2) NOT NULL DEFAULT 0.00, PRIMARY KEY (id), UNIQUE KEY uk_user_stock (user_id, stock_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE trade_record ( id INT NOT NULL AUTO_INCREMENT, user_id INT NOT NULL, stock_id INT NOT NULL, trade_type VARCHAR(4) NOT NULL COMMENT BUY or SELL, price DECIMAL(10,2) NOT NULL, quantity INT NOT NULL, amount DECIMAL(15,2) NOT NULL, trade_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;position 表上的唯一键 uk_user_stock 保证了同一个用户对同一只股票只会有一行记录新增时用 ON DUPLICATE KEY UPDATE或者先 select 后 insert/update。后者更直观也方便在 MyBatis 里走两条语句。3. 用 MySQL 实现股票交易核心逻辑建库、事务与存储过程3.1 初始化数据库与账号权限动手写代码前先把 MySQL 里的数据库和账号建好。常见做法是创建一个独立账号专门给这个应用用不要直接用 root防止连错库。建库时指定字符集 utf8mb4避免中文乱码排序规则用 utf8mb4_general_ci 就够。CREATE DATABASE IF NOT EXISTS stock_trade DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; CREATE USER stock_applocalhost IDENTIFIED BY Stock2024; GRANT SELECT, INSERT, UPDATE, DELETE ON stock_trade.* TO stock_applocalhost; FLUSH PRIVILEGES;这里的 GRANT 只给了增删改查权限没有给 DDL防止应用账号误操作表结构。生产环境通常还会限制连接来源 IP但本地开发用 localhost 够用。如果你的 MySQL 版本较高create user 和 grant 是分开执行的注意顺序。3.2 买入/卖出操作的 SQL 事务脚本买卖股票的核心是事务脚本不依赖任何框架时可以直接在 MySQL 客户端里验证逻辑是否正确。以买入为例假设用户 id1股票 id3数量 100当前价格 10.50START TRANSACTION; -- 锁定用户行防止并发修改余额 SELECT balance FROM user WHERE id 1 FOR UPDATE; -- 这里用程序判断余额是否足够如果不足则 ROLLBACK UPDATE user SET balance balance - 1050.00 WHERE id 1; -- 尝试插入持仓如果已有记录则更新数量 INSERT INTO position (user_id, stock_id, quantity, available_quantity, cost_price) VALUES (1, 3, 100, 100, 10.50) ON DUPLICATE KEY UPDATE quantity quantity 100, available_quantity available_quantity 100, cost_price (cost_price * quantity 10.50 * 100) / (quantity 100);注意ON DUPLICATE KEY UPDATE里的 quantity 在等号右侧指的是更新前的值所以整个表达式是先算出新的总成本再除以新数量得到摊薄后的成本价。最后插入交易流水并提交INSERT INTO trade_record (user_id, stock_id, trade_type, price, quantity, amount) VALUES (1, 3, BUY, 10.50, 100, 1050.00); COMMIT;如果余额不足或股票不存在则执行ROLLBACK。关键点是那句SELECT ... FOR UPDATE。为什么要在事务里手动锁行因为如果不锁两个并发请求同时读到余额为 10000各自都认为可以买入 1000 元的股票最后余额可能变负数。FOR UPDATE让行级排他锁在事务提交或回滚前一直持有后面到达的事务只能等待。卖出逻辑类似只是方向相反但多一个步骤先锁定持仓行SELECT ... FOR UPDATE判断可用数量是否足够然后扣减持仓、增加余额、写流水。这里要特别处理一个问题如果卖出的数量把持仓清零是保留 position 的一行 0 记录还是直接删除我建议删除否则持仓列表会累积一堆数量为 0 的空行前端还要过滤。3.3 存储过程与触发器计算可用余额和持仓成本毕设里用不用存储过程可以自由选择但如果要展示对 MySQL 的掌握程度建议写一个存储过程来封装买入操作。存储过程的好处是事务逻辑在数据库端保持原子应用层调用一次即可。下面是一个简化版DELIMITER $$ CREATE PROCEDURE sp_buy_stock( IN p_user_id INT, IN p_stock_id INT, IN p_quantity INT, IN p_price DECIMAL(10,2), OUT p_result INT ) BEGIN DECLARE v_balance DECIMAL(15,2); DECLARE v_amount DECIMAL(15,2) DEFAULT p_quantity * p_price; DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; SET p_result -1; END; START TRANSACTION; SELECT balance INTO v_balance FROM user WHERE id p_user_id FOR UPDATE; IF v_balance v_amount THEN ROLLBACK; SET p_result -2; ELSE UPDATE user SET balance balance - v_amount WHERE id p_user_id; INSERT INTO position (user_id, stock_id, quantity, available_quantity, cost_price) VALUES (p_user_id, p_stock_id, p_quantity, p_quantity, p_price) ON DUPLICATE KEY UPDATE quantity quantity p_quantity, available_quantity available_quantity p_quantity, cost_price (cost_price * quantity p_price * p_quantity) / (quantity p_quantity); INSERT INTO trade_record (user_id, stock_id, trade_type, price, quantity, amount) VALUES (p_user_id, p_stock_id, BUY, p_price, p_quantity, v_amount); COMMIT; SET p_result 0; END IF; END$$ DELIMITER ;参数说明p_user_id、p_stock_id 是外键p_quantity 为股数p_price 为当前价p_result 是输出参数0 代表成功-1 是数据库异常-2 是余额不足。这个存储过程把事务、行锁、异常处理都放在了数据库端应用层只需要一行 Class.forName CallableStatement 调用。但注意存储过程可维护性差不适合复杂业务。毕设中作为加分项没有问题但主要事务逻辑还是应该写在 Service 层用 Transactional 管理。触发器一般不推荐用于核心交易逻辑因为隐式执行难以排查。如果非要用可以只加一个审计触发器在 trade_record 插入时把变动写入一张 daily_summary 表用于统计每日交易额满足一下「用到了触发器」的毕设要求。4. SSM 三层整合配置与关键业务代码从 Mapper 到 Controller4.1 applicationContext.xml、springmvc.xml 和 mybatis-config.xml 的要点SSM 整合最怕配错路径先把三份配置文件的作用理清。applicationContext.xml 放在 WEB-INF 下负责 Spring 容器数据源、事务管理器、Mapper 扫描。springmvc.xml 负责 SpringMvc 控制器包扫描、注解驱动、视图解析器。mybatis-config.xml 放 MyBatis 全局配置类型别名、驼峰映射、日志。用 Maven 管理时pom.xml 里加 spring-webmvc、mybatis、mybatis-spring、mysql-connector-java、druid 依赖版本用 5.2.x 或 5.3.x 均可。applicationContext.xml 的核心片段context:property-placeholder locationclasspath:jdbc.properties/ bean iddataSource classcom.alibaba.druid.pool.DruidDataSource init-methodinit destroy-methodclose property namedriverClassName value${jdbc.driver}/ property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property nameconfigLocation valueclasspath:mybatis-config.xml/ property namemapperLocations valueclasspath:mapper/*.xml/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.stock.mapper/ /bean bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean tx:annotation-driven transaction-managertransactionManager/逻辑说明property-placeholder 加载数据库连接参数Druid 数据源负责连接池。SqlSessionFactoryBean 把 MyBatis 的 SQL 映射文件指向 classpath:mapper/ 目录同时指定全局配置文件。MapperScannerConfigurer 会扫描指定包下的接口把它们注册成 Spring Bean这样 Service 里直接 Autowired 就能拿到 Mapper。事务管理器绑定了同一个数据源保证事务边界生效。springmvc.xml 里除了包扫描和注解驱动还要配视图解析器context:component-scan base-packagecom.stock.controller/ mvc:annotation-driven/ bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/views// property namesuffix value.jsp/ /bean mvc:default-servlet-handler/防止静态资源被 DispatcherServlet 拦截是个坑default-servlet-handler 解决这个烦人的问题。JSP 放进 /WEB-INF/views 下外部直接访问不到只能通过 controller 转发。4.2 用 MyBatis 写查询多表关联和动态 SQLMyBatis 的映射文件直接对应业务查询。股票列表页需要展示股票代码、名称和当前价这些在 stock 表里都有。持仓页需要关联 user 和 stock 查出持仓详情select idselectPositionsByUserId resultTypecom.stock.pojo.PositionVO SELECT p.id, p.user_id AS userId, p.stock_id AS stockId, s.stock_code AS stockCode, s.stock_name AS stockName, p.quantity, p.available_quantity AS availableQuantity, p.cost_price AS costPrice, s.current_price AS currentPrice, (s.current_price - p.cost_price) * p.quantity AS profit FROM position p INNER JOIN stock s ON p.stock_id s.id WHERE p.user_id #{userId} /select这里用 resultType 映射到 VO 对象避免了繁琐的 resultMap 配置。注意把表字段的 snake_case 转成驼峰字段名可以在 mybatis-config.xml 里开启mapUnderscoreToCamelCase。动态 SQL 一般用在查询条件的组合上例如按股票代码和交易类型筛流水select idselectTradeRecords resultTypecom.stock.pojo.TradeRecord SELECT * FROM trade_record WHERE user_id #{userId} if teststockCode ! null and stockCode ! AND stock_id (SELECT id FROM stock WHERE stock_code #{stockCode}) /if if testtradeType ! null and tradeType ! AND trade_type #{tradeType} /if ORDER BY trade_time DESC /selectif标签拼条件时注意每个条件前面要加 AND 或 OR而且第一个条件左边不能是 WHERE 后跟 AND 这样的错误。MyBatis 的#{}是预编译占位符会在 SQL 里生成 ?能防 SQL 注入${}是直接拼字符串只能用在表名、排序字段这种无法占位的地方用之前必须严格过滤白名单。4.3 控制器层实现股票下单接口的参数校验与异常处理Controller 不能写业务逻辑只做参数绑定和结果转发。用 SpringMvc 的注解来接收前端表单参数Controller RequestMapping(/trade) public class TradeController { Autowired private TradeService tradeService; RequestMapping(value /buy, method RequestMethod.POST) ResponseBody public Result buy(RequestParam(userId) Integer userId, RequestParam(stockId) Integer stockId, RequestParam(quantity) Integer quantity) { if (quantity null || quantity 0) { return Result.error(购买数量必须为正整数); } try { tradeService.buyStock(userId, stockId, quantity); return Result.success(买入成功); } catch (BusinessException e) { return Result.error(e.getMessage()); } } }这段代码做了两层防护Controller 做基础参数校验Service 里做业务校验比如余额不足、持仓不够都抛自定义 BusinessException。Service 方法上的Transactional是事务生效的开关Service public class TradeServiceImpl implements TradeService { Autowired private UserMapper userMapper; Autowired private PositionMapper positionMapper; Autowired private TradeRecordMapper tradeRecordMapper; Override Transactional(rollbackFor Exception.class) public void buyStock(Integer userId, Integer stockId, Integer quantity) { Stock stock stockMapper.selectByPrimaryKey(stockId); if (stock null) { throw new BusinessException(股票不存在); } BigDecimal amount stock.getCurrentPrice().multiply(new BigDecimal(quantity)); User user userMapper.selectByPrimaryKeyForUpdate(userId); if (user.getBalance().compareTo(amount) 0) { throw new BusinessException(余额不足); } userMapper.decreaseBalance(userId, amount); positionMapper.insertOrUpdatePosition(userId, stockId, quantity, stock.getCurrentPrice()); tradeRecordMapper.insertTradeRecord(userId, stockId, BUY, stock.getCurrentPrice(), quantity, amount); } }rollbackFor Exception.class表示任何异常都会回滚不只是 RuntimeException。很多人把 Transactional 加在接口上其实加在实现类上更准确因为 Spring 的代理机制是基于类的 CGLIB 或基于接口的 JDK 动态代理这导致自调用时事务失效。所以不要在同一个类里从 buy 方法调用另一个带 Transactional 方法而是拆到不同 Service 或注入自身代理。5. 部署、验证与答辩用 SQL 和压测证明系统可靠5.1 Maven 打包和 Tomcat 部署的操作顺序先在 pom.xml 里确认打包方式是 war然后执行mvn clean package。如果第一次构建拉依赖很慢就配阿里云镜像。打包结束后target 目录下会生成stock-trade.war把这个 war 复制到 Tomcat 的 webapps 下启动即可自动解压。部署前要检查jdbc.properties里的数据库地址是不是本机Tomcat 的 JDK 版本和项目编译版本是否一致不然启动时会报 UnsupportedClassVersionError。5.2 用三条 SQL 验证数据一致性系统跑起来后不要只看页面显示正常要直接查数据库验证。以下三条查询是验证数据一致性的基础。-- 1. 验证用户余额与交易流水是否匹配 SELECT u.id, u.balance, (SELECT IFNULL(SUM(CASE WHEN trade_typeBUY THEN amount ELSE -amount END), 0) FROM trade_record WHERE user_id u.id) AS trade_delta FROM user u; -- 2. 验证持仓数量与卖出流水是否冲突 SELECT p.user_id, p.stock_id, p.quantity, (SELECT IFNULL(SUM(IF(trade_typeSELL, -quantity, quantity)), 0) FROM trade_record WHERE user_id p.user_id AND stock_id p.stock_id) AS net_quantity FROM position p;第一条查询算出用户所有买入和卖出流水的净金额如果初始余额字段存在那么balance 净买入金额应该等于初始值不一致说明扣款逻辑有 bug。第二条验证持仓数量是交易流水的净累加量如果对不上说明并发下丢了更新。这两个查询在答辩时展示出来比空口说系统正常有说服力得多。5.3 并发下单的压测技巧和常见错误用 JMeter 或写一个简单的 Java 多线程程序同时发起 50 个买入请求看最终库存和余额是否超额。最容易暴露的问题是超卖两个线程同时读到同一份持仓各减 1结果数量只减了 1。解决办法就是前面说的SELECT ... FOR UPDATE。压测时观察 MySQL 是否有死锁日志如果频繁出现死锁检查事务里是否按相同的顺序锁定 user 和 position避免两个事务交叉锁行。答辩时经常被问MyBatis 的#{}和${}有什么区别事务注解为什么没生效数据库金额用什么类型这些答案都在前面的代码里。说明文档和 LW 的编写要点我已经整理成下面的表格按这个结构写内容不会散文档章节必须包含的内容常见错误系统概述项目背景、开发环境、技术选型大段抄百度百科数据库设计ER 图、表结构说明、字段约束只有建表语句没有关系说明功能实现每个模块的时序图/流程图贴整个 Controller 代码占篇幅测试报告功能测试用例、并发压测结果只说“测试通过”最后一章留给一个具体技巧在 Service 层加一个 AOP 切面环绕通知里打印每个交易方法的入参、耗时和返回值并把异常堆栈记录到日志文件。这样答辩时能展示业务有问题也能快速定位。写日志时注意不要打印用户密码金额脱敏处理。本文还有配套的精品资源点击获取
返回列表