Java量化系列(四):实现自选股票维护功能

在前三篇文章中,我们已完成量化系统"数据层"的完整搭建------包括股票基础列表同步、每日日K增量同步、历史K线存量同步。有了完备的股票数据后,接下来需要面向用户层面构建核心交互功能:自选股票维护。毕竟对于量化交易者而言,关注的股票往往是少数几只,自选功能能帮助用户快速聚焦核心标的,便捷查看其数据与策略表现。

本文作为Java量化系列的第四篇,将聚焦"自选股票维护"核心模块,基于SpringBoot 3.3.8 + Mybatis-Plus + Mysql8.0技术栈,实现自选股票的查询、添加、删除、编辑笔记四大基础功能。同时涵盖表结构设计、同样适合三年以上Java开发经验,希望完善量化系统用户交互层的工程师参考。

一、核心需求与表结构设计

自选股票维护的核心诉求是"用户隔离""操作便捷""数据一致",结合量化系统的用户使用场景,我们先明确需求边界,再设计适配的表结构。

1.1 核心需求边界

  • 用户隔离:不同用户的自选股票独立存储,通过user_id字段区分,避免数据混淆;
  • 基础操作:支持四大核心功能------查询(按关键词模糊搜索、分页)、添加(校验股票有效性、避免重复)、删除(按股票代码移除)、编辑笔记(记录标的关注原因、策略要点等);
  • 状态管控:支持自选股票的"启用/禁用"状态(status字段)和"逻辑删除"(flag字段),避免物理删除导致数据丢失;
  • 异常兼容:添加时校验股票是否存在,删除时清理关联缓存,编辑时校验必填字段,确保操作合法性。

1.2 表结构设计:stock_selected

基于需求设计自选股票表stock_selected,兼顾用户隔离、状态管控和扩展需求,具体SQL如下:

复制代码
CREATE TABLE `stock_selected` (
  `id` int NOT NULL AUTO_INCREMENT COMMENT '主键自增',
  `stock_code` varchar(8) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT '股票的编号',
  `stock_name` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '股票的名称',
  `user_id` int DEFAULT NULL COMMENT '用户的id',
  `create_time` timestamp NULL DEFAULT NULL COMMENT '创建时间',
  `code_notes` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '笔记',
  `status` int NOT NULL COMMENT '状态 0为禁用 1为启用',
  `flag` int DEFAULT NULL COMMENT '1是正常0为删除',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=520 DEFAULT CHARSET=utf8mb3 ROW_FORMAT=DYNAMIC COMMENT='股票自选表,是用户自己选择的';

关键字段说明:

  • user_id:核心隔离字段,关联用户表主键,确保不同用户的自选数据独立;
  • stock_code:股票编号(如001318、600000),关联股票基础表的code字段;
  • code_notes:笔记字段,支持用户记录关注该股票的原因(如"新能源赛道龙头""均线策略标的");
  • status:状态字段(0禁用/1启用),支持用户隐藏部分自选股票但不删除;
  • flag:逻辑删除标识(1正常/0删除),避免物理删除导致历史数据无法追溯;
  • create_time:创建时间,便于用户查看自选股票的添加时间线。

1.3 核心对象映射:DO

遵循"分层设计"原则,定义核心对象完成请求参数接收、数据库交互、响应结果返回的流转,核心对象如下:

1.3.1 数据库对象:StockSelectedDo

stock_selected表字段一一对应,用于Mybatis-Plus数据库交互,核心代码:

复制代码
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("stock_selected")
public class StockSelectedDo implements Serializable {

    private static final long serialVersionUID = 1L;

    /** 主键自增 */
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    /** 股票的编号 */
    @TableField("stock_code")
    private String stockCode;

    /** 股票的名称 */
    @TableField("stock_name")
    private String stockName;

    /** 用户的id */
    @TableField("user_id")
    private Integer userId;

    /** 创建时间 */
    @TableField("create_time")
    private Date createTime;

    /** 笔记(对应表中code_notes字段) */
    @TableField("code_notes")
    private String notes;

    /** 是否启用,禁用  1为启用,0为禁用 */
    @TableField("status")
    private Integer status;

    /** 是否删除 1为正常 0为删除(表中flag字段定义) */
    @TableField("flag")
    private Integer flag;
}

关键说明:通过@TableName关联数据库表,@TableId指定主键策略(自增),@TableField关联表字段(notes对应表中code_notes,需显式指定字段名)。

1.3.2 请求对象:StockSelectedRo

接收前端请求参数(如查询条件、添加信息、编辑笔记内容),核心字段包括userId(当前登录用户ID)、stockCode(股票代码)、notes(笔记)等,按需扩展查询关键词字段。

1.3.3 响应对象:StockSelectedVo

返回前端展示数据,在StockSelectedDo基础上屏蔽数据库字段(如flag逻辑删除标识),仅暴露用户需查看的字段(idstockCodestockNamenotesstatuscreateTime)。

二、核心流程设计:四大功能的完整链路

主要是对单表的进行维护, 故只列出关键的逻辑。

自选股票维护的四大核心功能,均遵循"请求参数校验 → 业务逻辑处理 → 数据库交互 → 结果返回"的链路,不同功能的核心差异在于业务逻辑校验环节(如添加需校验股票存在性和重复性,删除需清理缓存)。整体流程概括如下:

复制代码
前端请求 → 控制器接收参数 → 拦截器获取当前用户ID → 业务层校验参数合法性 → 数据层交互(查/增/删/改) → 响应结果返回

三、核心实现:分步拆解四大功能代码

3.1 控制器层:接口定义与参数预处理

首先定义自选股票维护的核心接口,采用RESTful风格,通过POST请求接收参数,同时在控制器层完成基础参数校验(如股票代码非空)和当前用户ID注入(通过getUserId()方法从上下文获取,需集成用户登录认证功能)。

核心代码实现:

复制代码
@RestController
@RequestMapping("/stock/selected")
public class StockSelectedController {

    @Resource
    private StockSelectedBusiness stockSelectedBusiness;

    /**
     * 1. 查询用户自选股票(支持分页、关键词模糊搜索)
     * @param stockSelectedRo 包含查询条件(股票代码、名称、状态等)的请求对象
     * @return 分页后的自选股票列表
     */
    @Operation(summary = "查询用户自选股票")
    @PostMapping("/list")
    public OutputResult<PageResponse<StockSelectedVo>> list(@RequestBody StockSelectedRo stockSelectedRo) {
        // 注入当前登录用户ID,确保数据隔离
        stockSelectedRo.setUserId(getUserId());
        return stockSelectedBusiness.listSelected(stockSelectedRo);
    }

    /**
     * 2. 添加股票到自选表
     * @param stockSelectedRo 包含股票代码的请求对象
     * @return 添加结果(成功/失败提示)
     */
    @Operation(summary = "添加到自选表")
    @PostMapping("/add")
    public OutputResult add(@RequestBody StockSelectedRo stockSelectedRo){
        // 基础校验:股票代码不能为空
        if (!StrUtil.isNotBlank(stockSelectedRo.getStockCode())){
            return OutputResult.buildAlert(ResultCode.STOCK_CODE_IS_EMPTY);
        }
        // 注入当前登录用户ID
        stockSelectedRo.setUserId(getUserId());
        return stockSelectedBusiness.add(stockSelectedRo);
    }

    /**
     * 3. 根据股票代码删除自选股票(股票页面专用)
     * @param codeRo 包含股票代码的请求对象
     * @return 删除结果(成功/失败提示)
     */
    @Operation(summary = "根据股票code进行移除,股票页面使用")
    @PostMapping("/deleteByCode")
    public OutputResult deleteByCode(@RequestBody CodeRo codeRo){
        // 基础校验:股票代码不能为空
        if (!StrUtil.isNotBlank(codeRo.getCode())){
            return OutputResult.buildAlert(ResultCode.STOCK_CODE_IS_EMPTY);
        }
        // 转换为自选股票请求对象,注入当前登录用户ID
        StockSelectedRo stockSelectedRo = new StockSelectedRo();
        stockSelectedRo.setUserId(getUserId());
        stockSelectedRo.setStockCode(codeRo.getCode());
        return stockSelectedBusiness.deleteByCode(stockSelectedRo);
    }

    /**
     * 4. 编辑自选股票笔记
     * @param stockSelectedRo 包含记录ID/股票代码、笔记内容的请求对象
     * @return 编辑结果(成功/失败提示)
     */
    @Operation(summary = "根据自选记录,编辑笔记")
    @PostMapping("/editNotes")
    public OutputResult editNotes(@RequestBody StockSelectedRo stockSelectedRo){
        // 注入当前登录用户ID
        stockSelectedRo.setUserId(getUserId());
        // 基础校验:必须指定记录ID或股票代码(定位要编辑的记录),笔记内容不能为空
        if (stockSelectedRo.getId() == null && !StrUtil.isNotBlank(stockSelectedRo.getStockCode())) {
            return OutputResult.buildAlert(ResultCode.ID_IS_EMPTY);
        }
        if (!StrUtil.isNotBlank(stockSelectedRo.getNotes())) {
            return OutputResult.buildAlert(ResultCode.STOCK_SELECTED_NOTES_EMPTY);
        }
        return stockSelectedBusiness.editNotes(stockSelectedRo);
    }

    /**
     * 从上下文获取当前登录用户ID
     */
    private Integer getUserId() {
        Integer userId = ThreadLocalUtils.getUserId();
        if (userId != null) {
            return userId;
        }
        return 1;
    }
}

关键注意点:

  • 用户ID注入:通过getUserId()方法从用户上下文获取当前登录用户ID,确保不同用户的自选数据隔离,这是自选功能的核心设计点;
  • 基础参数校验:在控制器层完成简单的非空校验(如股票代码、笔记内容),避免无效请求进入业务层,提升系统效率;
  • 接口文档:通过@Operation注解生成接口文档(集成Knife4j/Swagger),便于前后端联调。

3.2 业务层:核心逻辑实现(重点)

业务层是自选功能的核心,负责完成参数合法性校验、关联数据查询(如校验股票是否存在)、业务规则执行(如避免重复添加)、缓存处理等核心逻辑。下面按四大功能分别拆解:

3.2.1 功能一:查询用户自选股票

核心逻辑:根据当前用户ID,结合查询条件(如股票代码/名称关键词、状态)进行分页查询,支持模糊搜索(通过股票代码或名称匹配关键词)。

核心代码实现:

根据 股票编码 或者 股票名称 进行查询

3.2.2 功能二:添加股票到自选表

核心逻辑:添加前需完成三重校验------① 股票是否存在(通过股票代码/名称/完整代码查询基础股票表);② 该股票是否已添加到当前用户的自选表(避免重复);③ 自选股票数量是否超限(如最多添加100只)。校验通过后,插入自选表并默认设置状态为"启用"(status=1)。

核心代码实现:

复制代码
@Service
public class StockSelectedBusinessImpl implements StockSelectedBusiness {

    @Resource
    private StockSelectedService stockSelectedService;
    @Resource
    private StockService stockService;

    @Override
    public OutputResult add(StockSelectedRo stockSelectedRo) {
        try {
            // 1. 校验1:查询股票是否存在(支持通过股票代码、完整代码、名称查询)
            StockDto stockDto = stockService.getByCodeOrNameOrFullCode(stockSelectedRo.getStockCode());
            if (null == stockDto) {
                // 股票不存在,返回错误提示
                return OutputResult.buildAlert(ResultCode.STOCK_CODE_NO_EXIST);
            }

            // 2. 统一股票代码格式(避免因完整代码/简称导致的重复)
            stockSelectedRo.setStockCode(stockDto.getCode());

            // 3. 校验2:该股票是否已添加到当前用户的自选表(避免重复添加)
            OutputResult addValidateResult = stockSelectedService.validateAdd(stockSelectedRo, 100);
            if (!addValidateResult.getSuccess()) {
                return addValidateResult;
            }

            // 4. 校验通过,添加到自选表(默认状态:启用status=1,正常flag=1,创建时间为当前时间)
            stockSelectedService.add(stockSelectedRo, stockDto.getName());

            // 5. 返回添加成功提示(包含股票代码和名称)
            return OutputResult.buildSucc(stockDto.getCode() + "(名称:" + stockDto.getName() + ")添加到自选成功");
        } catch (Exception e) {
            log.error("用户 {} 添加自选股票 {} 失败", stockSelectedRo.getUserId(), stockSelectedRo.getStockCode(), e);
            return OutputResult.buildFail("添加自选股票失败:" + e.getMessage());
        }
    }
}

关键补充说明:

  • 股票存在性校验:通过stockService.getByCodeOrNameOrFullCode()方法查询基础股票表,支持股票代码、完整代码(如0.001318)、股票名称三种查询方式,提升用户操作便捷性;
  • 重复添加校验:stockSelectedService.validateAdd()方法内部通过"user_id + stock_code"组合查询,判断该股票是否已在当前用户的自选表中(且状态为正常);
  • 数量限制校验:validateAdd()方法第二个参数"100"为自选股票数量上限,避免用户添加过多标的导致查询效率下降。

3.2.3 功能三:根据股票代码删除自选股票

核心逻辑:删除前需查询股票是否存在,然后执行逻辑删除(更新flag字段为0,而非物理删除),同时清理该股票的价格缓存(如Redis中存储的实时价格),避免缓存数据不一致。

核心代码实现:

复制代码
@Service
public class StockSelectedBusinessImpl implements StockSelectedBusiness {

    @Resource
    private StockSelectedService stockSelectedService;
    @Resource
    private StockService stockService;
    @Resource
    private RedisUtil redisUtil;

    @Override
    public OutputResult deleteByCode(StockSelectedRo stockSelectedRo) {
        try {
            // 1. 校验股票是否存在,获取标准股票代码
            StockDto stockDto = stockService.getByCodeOrNameOrFullCode(stockSelectedRo.getStockCode());
            if (null == stockDto) {
                return OutputResult.buildAlert(ResultCode.STOCK_CODE_NO_EXIST);
            }
            String realCode = stockDto.getCode();
            stockSelectedRo.setStockCode(realCode);

            // 2. 执行逻辑删除(更新flag=0,而非物理删除)
            OutputResult deleteResult = stockSelectedService.deleteByCode(stockSelectedRo);
            if (!deleteResult.getSuccess()){
                return deleteResult;
            }

            // 3. 清理缓存:删除该股票的实时价格缓存(避免缓存残留)
            redisUtil.delete(Collections.singleton(Const.STOCK_PRICE + realCode));

            return OutputResult.buildSucc("删除自选股票 " + realCode + " 成功");
        } catch (Exception e) {
            log.error("用户 {} 删除自选股票 {} 失败", stockSelectedRo.getUserId(), stockSelectedRo.getStockCode(), e);
            return OutputResult.buildFail("删除自选股票失败:" + e.getMessage());
        }
    }
}

关键注意点:

  • 逻辑删除:通过更新flag字段为0实现逻辑删除,保留历史数据,便于后续数据统计和恢复;
  • 缓存清理:删除自选股票后,清理Redis中该股票的价格缓存(Const.STOCK_PRICE + 股票代码为缓存key),避免用户已删除自选股票但仍能看到缓存的价格数据,确保数据一致性;
  • 标准代码统一:通过stockService.getByCodeOrNameOrFullCode()获取标准股票代码,避免因用户输入完整代码(如0.001318)导致删除失败。

3.2.4 功能四:编辑自选股票笔记

核心逻辑:通过"记录ID"或"用户ID+股票代码"定位要编辑的自选记录,校验笔记内容非空后,更新notes字段(对应表中code_notes)。

核心代码实现:

修改 code_notes 表字段的信息

关键注意点:

  • 记录定位:支持两种定位方式------通过记录ID(精确)或"用户ID+股票代码"(兼容无ID场景,如股票页面直接编辑),提升操作灵活性;
  • 状态校验:仅允许编辑正常状态(flag=1)的自选记录,避免编辑已删除的记录;
  • 字段映射:注意notes字段对应表中code_notes,需确保StockSelectedDo@TableField注解配置正确。

四、核心优化点与生产环境适配

上述实现已满足自选股票维护的基础需求,但在生产环境中需补充以下优化点,确保系统稳定性、效率与用户体验:

  1. 缓存优化:查询用户自选股票列表时,添加Redis缓存(缓存key为USER_SELECTED_STOCK + userId),避免频繁查询数据库;添加/删除/编辑操作后,同步更新缓存(删除旧缓存或更新缓存内容);
  2. 权限控制:集成Spring Security/Shiro等认证框架,确保getUserId()方法能安全获取当前登录用户ID,避免未登录用户访问自选接口;
  3. 参数校验增强:使用JSR-380注解(如@NotBlank@NotNull)替代手动非空校验,结合全局异常处理器统一处理校验失败信息;
  4. 异步处理:对于批量添加/删除自选股票的场景(扩展功能),采用线程池异步处理,提升用户操作体验;
  5. 日志与监控:记录自选股票操作日志(用户ID、操作类型、股票代码、操作时间),便于问题排查和用户行为分析;集成监控工具(如Prometheus + Grafana)监控接口调用量和失败率。

五、系列文章预告

本文完成了量化系统"用户交互层"的核心模块------自选股票维护,实现了用户对核心标的的便捷管理。下一篇文章将聚焦 "获取股票的实时价格",为后续策略回测功能铺路。

最后,留一个思考问题:在自选股票列表查询场景中,如何设计缓存策略(如缓存过期时间、更新机制),才能在保证查询效率的同时,避免缓存与数据库数据不一致?欢迎在评论区交流~

相关推荐
短剑重铸之日3 小时前
7天读懂MySQL|Day 5:执行引擎与SQL优化
java·数据库·sql·mysql·架构
酒九鸠玖3 小时前
Java--多线程
java
Dreamboat-L4 小时前
云服务器上部署nginx
java·服务器·nginx
长安er4 小时前
LeetCode215/347/295 堆相关理论与题目
java·数据结构·算法·leetcode·
cici158745 小时前
C#实现三菱PLC通信
java·网络·c#
k***92166 小时前
【C++】继承和多态扩展学习
java·c++·学习
weixin_440730506 小时前
java结构语句学习
java·开发语言·学习
JIngJaneIL6 小时前
基于java+ vue医院管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot
Coder_Boy_6 小时前
Spring AI 源码大白话解析
java·人工智能·spring