futu_gateway/
stock_db.rs

1// 股票列表 SQLite 持久化
2//
3// 对齐 C++ SecListDBHelper:将股票列表存储在本地 SQLite 数据库中,
4// 重启时从本地加载,避免全量同步。
5
6use rusqlite::{params, Connection, Result as SqlResult};
7use std::path::PathBuf;
8use std::sync::Mutex;
9
10/// 数据库文件路径: ~/.futu-opend-rs/stock_data.db
11fn db_path() -> PathBuf {
12    let home = std::env::var("HOME")
13        .or_else(|_| std::env::var("USERPROFILE"))
14        .unwrap_or_else(|_| ".".to_string());
15    let mut path = PathBuf::from(home);
16    path.push(".futu-opend-rs");
17    path.push("stock_data.db");
18    path
19}
20
21/// 存储在 SQLite 中的股票信息
22#[derive(Debug, Clone)]
23pub struct DbStockItem {
24    pub stock_id: u64,
25    pub code: String,
26    pub name_sc: String,
27    pub market_code: u32,
28    pub instrument_type: u32,
29    pub lot_size: u32,
30    pub delisting: bool,
31    pub warrnt_stock_owner: u64,
32    pub no_search: bool,
33    pub listing_date: u32,
34}
35
36/// 股票列表数据库 (线程安全,Mutex 包装)
37pub struct StockDb {
38    conn: Mutex<Connection>,
39}
40
41// rusqlite::Connection 不是 Send,但包在 Mutex 里是安全的
42unsafe impl Send for StockDb {}
43unsafe impl Sync for StockDb {}
44
45impl StockDb {
46    /// 打开(或创建)数据库
47    pub fn open() -> SqlResult<Self> {
48        let path = db_path();
49        if let Some(parent) = path.parent() {
50            let _ = std::fs::create_dir_all(parent);
51        }
52        let conn = Connection::open(&path)?;
53        conn.execute_batch(
54            "PRAGMA journal_mode=WAL;
55             PRAGMA synchronous=NORMAL;
56             PRAGMA cache_size=-8000;",
57        )?;
58        conn.execute_batch(
59            "CREATE TABLE IF NOT EXISTS config (
60                key TEXT PRIMARY KEY,
61                value TEXT NOT NULL
62             );
63             CREATE TABLE IF NOT EXISTS securities (
64                stock_id INTEGER PRIMARY KEY,
65                code TEXT NOT NULL,
66                name_sc TEXT NOT NULL DEFAULT '',
67                market_code INTEGER NOT NULL DEFAULT 0,
68                instrument_type INTEGER NOT NULL DEFAULT 0,
69                lot_size INTEGER NOT NULL DEFAULT 0,
70                delisting INTEGER NOT NULL DEFAULT 0,
71                warrnt_stock_owner INTEGER NOT NULL DEFAULT 0,
72                no_search INTEGER NOT NULL DEFAULT 0,
73                listing_date INTEGER NOT NULL DEFAULT 0
74             );",
75        )?;
76        // 旧版 schema 可能没有 listing_date 列,尝试 ALTER TABLE 补上
77        let _ = conn.execute_batch(
78            "ALTER TABLE securities ADD COLUMN listing_date INTEGER NOT NULL DEFAULT 0;",
79        );
80        Ok(Self {
81            conn: Mutex::new(conn),
82        })
83    }
84
85    /// 获取 stock_list_version
86    pub fn get_version(&self) -> u64 {
87        let conn = self.conn.lock().unwrap();
88        conn.query_row(
89            "SELECT value FROM config WHERE key = 'stock_list_version'",
90            [],
91            |row| {
92                let s: String = row.get(0)?;
93                Ok(s.parse::<u64>().unwrap_or(0))
94            },
95        )
96        .unwrap_or(0)
97    }
98
99    /// 保存 stock_list_version
100    pub fn set_version(&self, version: u64) -> SqlResult<()> {
101        let conn = self.conn.lock().unwrap();
102        conn.execute(
103            "INSERT OR REPLACE INTO config (key, value) VALUES ('stock_list_version', ?1)",
104            params![version.to_string()],
105        )?;
106        Ok(())
107    }
108
109    /// 加载所有股票记录
110    pub fn load_all(&self) -> SqlResult<Vec<DbStockItem>> {
111        let conn = self.conn.lock().unwrap();
112        let mut stmt = conn.prepare(
113            "SELECT stock_id, code, name_sc, market_code, instrument_type, lot_size,
114                    delisting, warrnt_stock_owner, no_search, listing_date
115             FROM securities",
116        )?;
117        let items = stmt
118            .query_map([], |row| {
119                Ok(DbStockItem {
120                    stock_id: row.get::<_, i64>(0)? as u64,
121                    code: row.get(1)?,
122                    name_sc: row.get(2)?,
123                    market_code: row.get::<_, i32>(3)? as u32,
124                    instrument_type: row.get::<_, i32>(4)? as u32,
125                    lot_size: row.get::<_, i32>(5)? as u32,
126                    delisting: row.get::<_, i32>(6)? != 0,
127                    warrnt_stock_owner: row.get::<_, i64>(7)? as u64,
128                    no_search: row.get::<_, i32>(8)? != 0,
129                    listing_date: row.get::<_, i32>(9)? as u32,
130                })
131            })?
132            .filter_map(|r| r.ok())
133            .collect();
134        Ok(items)
135    }
136
137    /// 开始批量写入事务
138    pub fn begin_batch(&self) -> SqlResult<()> {
139        let conn = self.conn.lock().unwrap();
140        conn.execute_batch("BEGIN TRANSACTION")?;
141        Ok(())
142    }
143
144    /// 提交事务
145    pub fn commit_batch(&self) -> SqlResult<()> {
146        let conn = self.conn.lock().unwrap();
147        conn.execute_batch("COMMIT")?;
148        Ok(())
149    }
150
151    /// 插入或更新一条股票记录
152    pub fn upsert(&self, item: &DbStockItem) -> SqlResult<()> {
153        let conn = self.conn.lock().unwrap();
154        conn.execute(
155            "INSERT OR REPLACE INTO securities
156             (stock_id, code, name_sc, market_code, instrument_type, lot_size,
157              delisting, warrnt_stock_owner, no_search, listing_date)
158             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
159            params![
160                item.stock_id as i64,
161                item.code,
162                item.name_sc,
163                item.market_code as i32,
164                item.instrument_type as i32,
165                item.lot_size as i32,
166                item.delisting as i32,
167                item.warrnt_stock_owner as i64,
168                item.no_search as i32,
169                item.listing_date as i32,
170            ],
171        )?;
172        Ok(())
173    }
174
175    /// 删除一条股票记录
176    pub fn delete(&self, stock_id: u64) -> SqlResult<()> {
177        let conn = self.conn.lock().unwrap();
178        conn.execute(
179            "DELETE FROM securities WHERE stock_id = ?1",
180            params![stock_id as i64],
181        )?;
182        Ok(())
183    }
184
185    /// 获取记录总数
186    pub fn count(&self) -> u64 {
187        let conn = self.conn.lock().unwrap();
188        conn.query_row("SELECT COUNT(*) FROM securities", [], |row| {
189            row.get::<_, i64>(0)
190        })
191        .unwrap_or(0) as u64
192    }
193}