每年到了毕设季,总有一批同学找我聊同一个题目:老师,用Java写股票管理系统行不行?行,当然行,但真正把它做成一个能过查重、能跑通演示、能扛住答辩老师追问的系统,跟拿个开源项目改个LOGO是两回事。这篇文章我想把带学生做Java毕设时攒下的经验,尤其是股票管理系统这类题目的完整落地链路,一次性讲透,从选题评估、功能拆解、技术选型到数据库设计、核心代码、实测踩坑,最后再说说论文和答辩怎么准备。
这个题目适合谁?适合后端基础一般、想稳扎稳打完成毕设的同学,也适合想用“管理系统”作为敲门砖、顺便刷一刷Java基础(集合、事务、并发)的人。它的优点是数据模型足够标准、业务边界清晰、演示效果好;缺点是如果不控制范围,很容易做成一个“什么都有但什么都没做完”的半成品。下面我按自己做项目时的实际推进顺序来写,你可以直接照着这个思路走。
1. 为什么“股票管理系统”是毕设选题的性价比之选
1.1 这个选题天然适合展示基本功
先想明白一件事:毕设管理系统类题目千千万,为什么股票管理系统值得选?我的判断标准是“数据关系能否自然体现数据库设计能力”。股票系统里有用户、股票、行情、持仓、交易流水、资金账户、自选列表,这些实体之间的关联关系非常标准,画ER图、建表、写SQL都能拿出东西来。相比“图书管理系统”这种做了无数遍的题目,股票系统明显更有区分度;相比“秒杀系统”这类高并发题目,它又不需要复杂的中间件,工作量可控。
业务规则也足够清晰:注册后有模拟资金,买入扣钱加持仓,卖出减持仓加资金,每笔操作生成流水。这些规则说难不难,说简单又比纯CRUD多了一层业务逻辑,正是答辩时老师喜欢追问的部分。换句话说,这个题目做出的系统,既能体现“你会写代码”,又能体现“你懂一点业务”,两全其美。
1.2 开题前先给系统划好边界
我见过太多人把毕设题目想得过大:要实时行情、要股票预测、要社群交流、要做App……最后学期末交上来一个空壳。非常不建议这样。你可以在论文“展望”部分写这些,但系统里不要全做。
我给学生的建议是:把系统定位成“模拟交易教学演示系统”,明确以下边界:
- 不接入真实券商接口,不接第三方实时行情,行情数据用本地静态数据或批量导入的模拟数据。
- 交易撮合简化为“按当前最新价即时成交”,不去模拟真实盘口的买卖队列。
- 不做真实支付、不做资金存取,用户注册后自动发放一笔初始模拟资金。
- 涨跌停限制、T+1、手续费等规则可以作为扩展项,时间充裕再加,不加也不影响论文完整性。
把这些边界提前写进开题报告和需求分析,后面开发和写论文都会轻松很多。老师看到你把边界写得清楚,第一印象就是“这个学生思路清晰”。
1.3 一个明确的系统雏形是什么样
结合标题来看,这套股票管理系统至少要覆盖以下闭环:管理员维护股票基础信息与行情数据,用户可以注册登录,浏览股票列表和行情K线,把感兴趣的股票加入自选,用模拟资金买入、卖出,查看自己的持仓和交易流水,最后能在个人中心看到资产盈亏统计。
我把这个闭环作为整个项目的“骨架”。当你把闭环跑通,再去扩展用户管理、数据统计、公告发布等外围功能,系统的完整度已经足够撑起一篇本科论文。记住一个原则:先做纵向闭环,再做横向扩展,不要一上来就铺摊子。
2. 系统功能拆解与角色权限:先把“做什么”钉死
2.1 两类角色,权限边界清清楚楚
股票管理系统虽然功能多,但角色就两类:管理员和普通用户。权限设计越简单越好,不要把RBAC搞得花里胡哨,否则代码量和答辩复杂度都会上升。我常用的方案是给用户表加一个role字段,1表示管理员,0表示普通用户,后端拦截器按角色判断接口访问权限。
管理员能做的事:股票基础信息管理、行情数据录入与导入、用户列表查询与禁用、系统公告发布。普通用户能做的事:注册登录、浏览股票行情、维护自选列表、模拟买入卖出、查看持仓与交易记录、查看个人资金账户。核心原则是:普通用户只能动自己的数据,不能改系统和股票数据;管理员不做交易操作。这个原则写清楚后,数据库查询语句的WHERE user_id = ?条件就有了依据,不会出现越权bug。
2.2 功能模块清单
这里我列一份可以直接抄进开题报告的功能清单:
- 用户模块:注册、登录、信息修改、密码加密存储
- 股票管理模块:股票新增、编辑、删除、分页查询、关键字搜索
- 行情管理模块:录入每日行情数据、展示最近K线、模拟涨跌幅计算
- 自选股模块:添加自选、取消自选、展示自选列表及实时模拟价格
- 模拟交易模块:买入、卖出、持仓查询
- 资金账户模块:初始资金发放、资金变动流水、可用余额查询
- 交易流水模块:按时间倒序展示用户全部交易记录
- 系统管理模块:管理员对用户启停用、数据初始化重置
这8个模块做下来,代码量大概在1.5万行左右(含前端),工作量适中,正好适合一个学期。
2.3 关键业务规则要在编码前定下来
以下是股票系统最核心的业务规则,建议做成文档贴在自己电脑前:
- 买入:用户买入数量必须大于0;买入金额 = 成交价 × 数量;可用余额必须不小于买入金额;买入成功后,持仓数量增加,可用余额减少;交易流水记录“买入”。
- 卖出:卖出数量必须不大于该股票持仓数量;卖出金额 = 成交价 × 数量;卖出成功后,持仓数量减少,可用余额增加;交易流水记录“卖出”。
- 持仓均价更新:买入时,新均价 = (旧持仓市值 + 本次买入金额) / (旧持仓数量 + 本次买入数量);卖出时不改变持仓均价,只减少数量。
这些规则看起来简单,但它们是答辩时老师最常问的“业务逻辑细节”,也是代码里最容易出bug的地方。比如持仓均价,很多同学直接用(旧均价 + 成交价) / 2,这就是错的,后面算总盈亏时全部乱掉。建议先把规则写在文档里,再动手写代码。
3. 技术选型与项目结构:Spring Boot + MyBatis Plus 这套组合为什么稳
3.1 选型不是越新越好,主动权才是关键
每次有学生问我“能不能用最新的Spring Boot 3.x、JDK 21”,我都先反问一句:你能保证折腾环境不花掉两个星期吗?毕设的技术选型原则是“稳定靠谱、资料多、你熟悉”。目前Java毕设最稳的组合还是 Spring Boot 2.7.x + MyBatis Plus + MySQL 5.7/8.0,前端可以用 Vue 2 + Element UI,也可以直接用 Thymeleaf 服务端渲染。JDK 用 8 或 11 都行,不要为了赶新潮选一个你完全没接触过的版本。
有人会问:用JSP/Servlet行不行?也行,但你手写控制层、事务、分页的工作量会大很多,答辩时也容易被追问底层细节。Spring Boot的价值在于自动配置和约定优于配置,让你把精力集中在业务逻辑上,这是毕设最需要的。MyBatis Plus更适合这种单表CRUD占大头的项目,手写SQL只留给多表联合查询和复杂统计,开发效率能翻一倍。
3.2 推荐项目包结构
我强烈建议用标准的 Controller-Service-Mapper-Entity 四层结构,包名按功能模块划分。下面这棵目录树可以直接拿来用:
com.example.stock ├── StockApplication.java ├── common │ ├── Result.java // 统一返回结果 │ ├── ResultCode.java │ └── GlobalExceptionHandler.java ├── config │ ├── CorsConfig.java // 跨域配置 │ ├── JwtInterceptor.java // 登录拦截器 │ └── WebMvcConfig.java ├── controller │ ├── AuthController.java │ ├── UserController.java │ ├── StockController.java │ ├── TradeController.java │ ├── PositionController.java │ ├── HistoryController.java │ └── AdminController.java ├── service │ ├── UserService.java │ ├── StockService.java │ ├── TradeService.java │ ├── PositionService.java │ └── DashboardService.java ├── mapper │ ├── UserMapper.java │ ├── StockMapper.java │ ├── StockQuoteMapper.java │ ├── PositionMapper.java │ └── TradeRecordMapper.java ├── entity │ ├── User.java │ ├── Stock.java │ ├── StockQuote.java │ ├── Position.java │ └── TradeRecord.java └── dto ├── BuyRequest.java ├── SellRequest.java └── LoginRequest.java按模块划分的好处是:论文里的“系统设计”章节可以直接引用这个结构,画包图、类图都方便;答辩被问“某个功能在哪”时,一眼就能指出来。很多同学喜欢按技术层分包(controller包放所有控制器),项目一大了找文件能找到怀疑人生,别踩这个坑。
3.3 核心依赖与配置,照抄即可
pom.xml 关键依赖如下:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>1.15</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <scope>provided</scope> </dependency> </dependencies>注意一点:Spring Boot 2.7.x 搭配的 jjwt 0.9.1 缺javax.xml.bind依赖,JDK8以上会报错,记得补上:
<dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.1</version> </dependency>application.yml 关键配置:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/stock_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 jwt: secret: your-secret-key expire: 604800serverTimezone=Asia/Shanghai这个配置必须写,不然数据库时间会比当前时间差8小时,后面踩坑部分我会详细说。
4. 数据库设计:股票系统最容易被问倒的是“钱怎么算”
4.1 七张核心表,别贪多
我把这套系统的表精简为7张,足够支撑全部功能:
| 表名 | 用途 | 关键字段 |
|---|---|---|
| sys_user | 用户表 | id, username, password, role, status, balance, create_time |
| stock_info | 股票基础信息表 | id, stock_code, stock_name, industry, total_market_cap, status |
| stock_quote | 行情数据表 | id, stock_code, trade_date, open_price, close_price, high_price, low_price, volume |
| stock_position | 持仓表 | id, user_id, stock_code, stock_name, quantity, avg_price, update_time |
| trade_record | 交易流水表 | id, user_id, stock_code, stock_name, trade_type, price, quantity, amount, trade_time |
| user_watchlist | 自选表 | id, user_id, stock_code, create_time |
| sys_notice | 公告表 | id, title, content, create_time |
这套设计覆盖了“股票数据管理、用户操作、交易记录”三个层面,ER图也容易画。不要额外加太多冗余表,否则写代码和写论文都是负担。
4.2 金额字段为什么必须用 decimal,踩过坑的人都懂
这是一个答辩高频问题,也是实际开发中最容易出bug的地方。如果用double存金额,买入0.1万股、价格3.14159时,计算出来的金额会变成314.15899999999996,给用户展示非常难看,累计盈亏统计多了还会出现几分钱的误差。正确做法是统一使用decimal类型。我建议的字段定义是:
- 金额字段:
decimal(18,2),本金、成交金额、余额都用它 - 价格字段:
decimal(10,3),股票价格保留3位小数足够 - 数量字段:
int或decimal(10,0),因为A股交易单位是“股”,取整即可 - 均价字段:
decimal(10,3),均价保留3位,展示时自己再四舍五入到2位
Java代码层面,所有金额计算统一使用BigDecimal,不要用double运算然后再转。我在下面的代码实现章节里会专门写。
4.3 核心建表SQL,附初始化数据
CREATE TABLE `sys_user` ( `id` int NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL, `password` varchar(64) NOT NULL, `role` tinyint DEFAULT '0' COMMENT '0普通用户,1管理员', `status` tinyint DEFAULT '1' COMMENT '1启用,0禁用', `balance` decimal(18,2) DEFAULT '1000000.00' COMMENT '模拟资金余额', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;CREATE TABLE `stock_position` ( `id` int NOT NULL AUTO_INCREMENT, `user_id` int NOT NULL, `stock_code` varchar(10) NOT NULL, `stock_name` varchar(50) NOT NULL, `quantity` int NOT NULL DEFAULT '0' COMMENT '持仓数量', `avg_price` decimal(10,3) NOT NULL DEFAULT '0.000' COMMENT '持仓均价', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_user_stock` (`user_id`,`stock_code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;CREATE TABLE `trade_record` ( `id` int NOT NULL AUTO_INCREMENT, `user_id` int NOT NULL, `stock_code` varchar(10) NOT NULL, `stock_name` varchar(50) NOT NULL, `trade_type` varchar(10) NOT NULL COMMENT 'BUY/SELL', `price` decimal(10,3) NOT NULL, `quantity` int NOT NULL, `amount` decimal(18,2) NOT NULL COMMENT '实际成交金额', `trade_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;初始化数据时,我会插入一个管理员账号(密码用MD5或BCrypt加密)、若干个正常的用户,以及沪深两市约20只常见股票的模拟行情。没有演示数据,答辩时系统打开是一片空白,那场面太尴尬了。
4.4 数据初始化技巧:用 CommandLineRunner 还是 SQL 脚本?
两种都可以,我更推荐在resources目录下放data.sql,配合spring.sql.init配置,应用启动时自动执行。也可以写一个DataInitializer类实现ApplicationRunner,启动时检查用户表是否有数据,没有就用代码插入。用代码初始化的好处是可以在里面加密密码生成,不用手工处理SQL里的哈希值。
我实际用的是“SQL脚本初始化基础数据 + 代码初始化行情演示数据”的组合,因为行情数据往往需要按日期批量生成,SQL写起来太累,代码里循环生成更轻松。答辩时你可以说这是“方便演示的模拟数据”,老师能理解,本来这就是模拟系统。
5. 核心代码实现与关键逻辑:从登录到下单
5.1 JWT登录与拦截器,密码必须加密存储
密码绝对不能明文存数据库,这是涉及用户信息安全的基本底线。建议用BCryptPasswordEncoder(Spring Security自带)或MD5 + salt。用Spring Security只是为了它的加密工具,不用接管整个安全框架,不然过滤器链会把你折腾崩溃。我用DigestUtils.md5DigestAsHex做一次简单加盐处理,演示够用,但答辩时说清楚“生产环境会用BCrypt”就稳了。
登录接口逻辑:前端传用户名和密码,后端查用户,校验密码,生成JWT返回前端。JWT里只放 userId 和 role,设置过期时间。拦截器里从请求头取token,解析成功才放行,失败返回401。
@Component public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if ("OPTIONS".equals(request.getMethod())) { return true; } String token = request.getHeader("Authorization"); if (token != null && token.startsWith("Bearer ")) { token = token.substring(7); String userId = JwtUtil.parseToken(token); if (userId != null) { request.setAttribute("userId", userId); return true; } } response.setStatus(401); request.getRequestDispatcher("/common/unauthorized").forward(request, response); return false; } }5.2 股票列表分页与搜索,MyBatis Plus 一行搞定
股票列表无非是按代码或名称模糊搜索,再分页返回。用 MyBatis Plus 的LambdaQueryWrapper就行:
public Page<Stock> pageStocks(int current, int size, String keyword) { LambdaQueryWrapper<Stock> wrapper = new LambdaQueryWrapper<>(); if (StringUtils.hasText(keyword)) { wrapper.like(Stock::getStockCode, keyword) .or() .like(Stock::getStockName, keyword); } wrapper.orderByAsc(Stock::getStockCode); return stockMapper.selectPage(new Page<>(current, size), wrapper); }这里有个小细节:搜索的关键字可能会同时包含汉字和数字,如果既按代码匹配又按名称匹配,要注意or的括号问题,否则会把条件“或”到别的条件上。用wrapper.and(w -> w.like(...).or().like(...))包一层更稳妥。
5.3 模拟买入卖出的核心逻辑:事务、锁、BigDecimal 一个不能少
这是整个系统技术含量最高的地方,也是答辩时最值得讲的地方。买入接口需要做四件事:查用户余额、查最新价、校验余额是否足够、扣减余额并增加持仓。这里最大的坑是并发问题:如果用户同时发起两笔买入请求,两个线程都读到余额够,然后一起扣款,余额就变负数了。
我用的方案是数据库行锁配合事务。在查询用户余额时加select ... for update,把这条用户记录锁住,其他线程必须等当前事务提交后才能改。代码逻辑如下:
@Service public class TradeServiceImpl implements TradeService { @Resource private UserMapper userMapper; @Resource private StockQuoteMapper stockQuoteMapper; @Resource private PositionMapper positionMapper; @Resource private TradeRecordMapper tradeRecordMapper; @Override @Transactional(rollbackFor = Exception.class) public void buy(BuyRequest req) { User user = userMapper.selectByIdForUpdate(req.getUserId()); if (user == null || user.getStatus() != 1) { throw new BusinessException("用户不存在或已被禁用"); } StockQuote quote = stockQuoteMapper.findLatestByStockCode(req.getStockCode()); if (quote == null) { throw new BusinessException("该股票暂无行情数据"); } BigDecimal price = quote.getClosePrice(); BigDecimal amount = price.multiply(BigDecimal.valueOf(req.getQuantity())); if (user.getBalance().compareTo(amount) < 0) { throw new BusinessException("可用余额不足,当前余额" + user.getBalance()); } user.setBalance(user.getBalance().subtract(amount)); userMapper.updateById(user); Position position = positionMapper.findByUserIdAndStockCode(req.getUserId(), req.getStockCode()); if (position == null) { Position newPosition = new Position(); newPosition.setUserId(req.getUserId()); newPosition.setStockCode(req.getStockCode()); newPosition.setStockName(req.getStockName()); newPosition.setQuantity(req.getQuantity()); newPosition.setAvgPrice(price); positionMapper.insert(newPosition); } else { // 新均价 = (旧持仓数量 * 旧均价 + 本次买入金额) / 新持仓数量 BigDecimal oldMarketValue = BigDecimal.valueOf(position.getQuantity()) .multiply(position.getAvgPrice()); BigDecimal newMarketValue = oldMarketValue.add(amount); int newQuantity = position.getQuantity() + req.getQuantity(); position.setQuantity(newQuantity); position.setAvgPrice(newMarketValue.divide(BigDecimal.valueOf(newQuantity), 3, RoundingMode.HALF_UP)); positionMapper.updateById(position); } TradeRecord record = new TradeRecord(); record.setUserId(req.getUserId()); record.setStockCode(req.getStockCode()); record.setStockName(req.getStockName()); record.setTradeType("BUY"); record.setPrice(price); record.setQuantity(req.getQuantity()); record.setAmount(amount); tradeRecordMapper.insert(record); } }这个代码里最关键的就是selectByIdForUpdate,在Mapper里是这么写的:
@Select("SELECT * FROM sys_user WHERE id = #{userId} FOR UPDATE") User selectByIdForUpdate(@Param("userId") Integer userId);为什么要锁用户行而不是锁股票行?因为余额是用户的,扣钱和加持仓必须保持一致性,锁用户行能确保同一用户的所有交易串行化,不同用户之间互不影响,并发性能也不会太差。这个细节答辩时主动讲出来,老师会觉得你是真懂并发控制。
卖出逻辑类似,核心是先查持仓,校验持有数量足够,更新持仓数量和用户余额。卖出不改变均价,所以只改数量不改avg_price。如果卖出后持仓数量为0,最好删掉持仓记录,这样持仓列表不会出现一堆数量为0的“僵尸股票”。
5.4 持仓与K线数据接口,前端展示要配图表
持仓接口查当前用户的持仓列表,每个持仓需要根据最新行情计算“当前市值”和“浮动盈亏”。这个用SQL联表或者Java里循环都能算,数据量小,循环简单直接。K线数据接口则从stock_quote表按日期升序取某只股票的记录,返回日期、开、收、高、低这五个字段,前端用ECharts的K线图组件直接画。建议后端返回字段名统一用英文字段(date, open, close, high, low),前端不用做额外映射,省事很多。
如果你想让系统看起来更有“实时感”,可以写一个定时任务,每30秒随机更新一下最近一条行情的收盘价,页面轮询刷新。这个属于锦上添花,加上之后演示效果会好很多。实现也不复杂,用@Scheduled(cron = "0/30 * * * * ?")注解就行。
6. 实测踩坑:从开发到答辩最容易翻车的5个问题
6.1 数据库时间差8小时,坑了多少人
症状:前端页面显示的交易时间比本地时间慢了8小时。原因:MySQL驱动连接串里没有指定serverTimezone,默认用了UTC,中国在东八区,所以差8小时。解决办法就是配置里写serverTimezone=Asia/Shanghai,同时MySQL的连接参数加上useSSL=false避免握手警告。还有一点:如果前端用到new Date()直接展示,后端返回LocalDateTime会带 T 格式,建议后端统一返回yyyy-MM-dd HH:mm:ss字符串,或者在Jackson配置里设置全局格式:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai6.2 金额计算出现一堆小数,根源是 double
我踩过的真实bug:用double计算 0.1 * 3.14159,结果不是 0.314159 而是0.31415899999999996。前端展示还好,但累计盈亏表里几个数一加,误差就出来了。修复方案是上面说的:数据库decimal,JavaBigDecimal,所有涉及金额的运算必须用BigDecimal,除法要指定精度和舍入模式。上线前的自测里,一定要拿几组极端数据验证:买入0.01元价格的股票、卖完所有持仓、余额刚好等于成交金额。
6.3 并发交易导致余额变负数,百试百灵
如果你把买接口写好,用Postman同时发两个请求,多半能复现余额变负数。原因就是两个事务同时读到同一个余额,各自判断“够”,然后都做减法。解决就靠锁:要么用@Transactional+select for update行锁,要么用乐观锁(表加version字段,更新时SET balance = balance - ? , version = version + 1 WHERE version = ?)。毕设系统并发量不大,行锁方案最简单,也方便答辩时讲清楚。这个坑建议你一定要测试,真出问题时,修复的过程都可以写进论文的“系统测试”章节,这是加分项。
6.4 前后端联调时的跨域问题
前后端分离开发时,前端跑在8081端口,后端8080端口,浏览器会拦截跨域请求。解决办法是后端加CORS配置,最省事的写法是:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }注意allowedOrigins("*")和allowCredentials(true)不能同时使用,会报错。用allowedOriginPatterns("*")代替就行。如果部署时后端和前端同域,这个配置可以去掉,但开发阶段必须留着。
6.5 打包部署后接口404,静态资源也没了
很多同学开发时一切正常,一mvn package打成jar包再运行,页面打不开,接口404。原因通常是前端分离部署时,前端dist没有并到后端resources里。我的建议是:毕设阶段就别搞独立前端部署了,把前端build后的dist文件复制到src/main/resources/static目录下,和Spring Boot一起打包,启动后直接访问http://localhost:8080就是完整系统,演示和答辩都非常方便。这个操作放在开发最后阶段做,前期开发用前后端分离模式效率更高。
7. 源码如何变成论文与答辩演示:过来人的几点建议
7.1 论文结构别从绪论硬憋,先写图表再补文字
我见过许多同学先写绪论,写了一天还在憋“研究背景”。我自己带学生的经验是反着来:先画ER图、功能结构图、架构图,再根据图表去写系统设计;系统实现章节直接对照代码截图和核心代码讲,最后再回头补绪论和技术介绍。这样写论文的速度至少快一倍,而且前后逻辑对得上。
本科论文的章节顺序通常是:绪论(背景、意义、国内外现状)、相关技术介绍(Java、Spring Boot、MySQL、MyBatis Plus)、需求分析(功能需求、非功能需求、用例图)、系统设计(总体架构、模块设计、数据库设计)、系统实现(分模块贴核心代码加界面截图)、系统测试(功能测试用例表、测试结果)。每一章都有模板可循,关键是不要出现“代码和描述不一致”的低级错误。
7.2 演示脚本要刻意设计,别现场乱点
答辩演示时间通常只有5到8分钟,你必须提前设计好“最佳路径”。我推荐的演示流程是:
- 启动后端,浏览器打开登录页,输入管理员账号登录。
- 展示股票列表和搜索功能,随便搜一个关键词。
- 进入某只股票详情页,展示K线图。
- 退出管理员,注册一个新用户或直接登录普通用户。
- 演示买入操作:选两只股票各买100股,展示余额变化和持仓变化。
- 演示卖出:卖出一部分,再展示持仓和交易流水。
- 打开个人中心,展示总资产和盈亏统计。
整个流程控制在5分钟内。现场演示最怕的就是“点哪个都没反应”,所以演示前至少完整走三遍,尤其是数据库要记得重置成初始状态,把之前测试的数据清掉。
7.3 答辩提问预判:用java基础就能应对的高频问题
答辩老师最常问的几类问题,其实都能在Java基础范围内找到答案:
- 为什么选Spring Boot?答:简化配置、快速搭建、生态成熟,适合快速开发管理系统。
- 事务是怎么实现的?答:用
@Transactional注解,底层是Spring的声明式事务管理,默认遇到运行时异常回滚。 - 怎么保证并发买不超扣?答:数据库行锁
select for update+ 事务,或者乐观锁版本号。 - 如果用户量变大,你这个系统有什么瓶颈?答:单库单表,性能瓶颈在数据库,可以引入Redis缓存热点行情,再考虑分库分表。这个答辩点可以作为论文的“扩展与展望”。
- 密码为什么不是明文?答:MD5加盐或BCrypt,防止拖库后用户密码泄露。
这些问题本质上就是在考你的Java面试基本功,平时刷过的HashMap、线程池、JVM调优之类的八股文在答辩时未必直接用得上,但基础的事务、锁、集合、异常处理必须说得清楚。建议答辩前把你自己写的Service层每个方法过一遍,做到“指哪讲哪”,比背十篇八股文都管用。
带过那么多轮毕设,我最有体会的一点是:股票管理系统这个题目的上限和下限都极高。下限是抄个CRUD框架换换皮,老师一眼看穿;上限是你能把资金计算、并发扣减、持仓均价这些业务细节讲出花来,再配合一套完整的数据设计和测试用例,就是妥妥的良到优。你不需要做什么高深的算法,也不需要分布式微服务,只需要把每一行代码的逻辑讲清楚、把每一个bug的修复过程记录明白,这个项目就已经超过了绝大多数同期作品。如果你正在为毕设发愁,希望这篇东西能帮你少走点弯路。