# UI 交互调用分析:06-数据存储SQLite > 分析日期:2026-09-01 | 依据:技术实现方案.md(接口定义) > 目的:基于 SQLite 存储层接口(strategy_holdings / market_quotes_cache + 持仓生命周期方法集),梳理**前端各操作 → 服务端 → 存储层**的完整调用链,确认对外 API 不变的前提下,存储层升级对调用流程的影响。 ## 调用链路总览(升级后不变的部分) ``` 前端组件(React) │ fetch POST /odl/api/ body={ args } ▼ connection.jsx useRpc() ── 标准化剥离 one-divine-lot/ 前缀 ▼ 服务端 src/api/*.js 领域分发(positions / strategies / qmt-connections / market / trades) ▼ PositionManager / MarketDataHub / Settings(业务逻辑) ▼ DataStore(改造)── 委托 ──▶ SqliteStore(node:sqlite,新增) │ ├─ strategy_holdings 表(持仓生命周期) │ └─ market_quotes_cache 表(行情快照) ``` **关键结论**:前端组件与 /odl/api/* 端点**零改动**;改动集中在服务端业务层(PositionManager 适配单票生命周期)+ 存储层(DataStore→SqliteStore)。以下逐操作分析。 --- ## 操作 1:策略持仓 tab 加载(读) **触发**:进入策略 tab → `StrategyTab.load()` **前端调用**(`StrategyTab.jsx`): ```js Promise.all([ call('one-divine-lot/strategy-positions', { args: { strategyId } }), // 策略持仓 call('one-divine-lot/unallocated', {}), // 未分配(添加下拉候选) ]); ``` **服务端**(`api/strategies.js`)→ `PositionManager.getStrategyPositions(strategyId)`: - 升级前:`getAllPositions()`(QMT REST)+ `storage.getDataset(strategyId)`(整策略读 store.json) - 升级后:`getAllPositions()` + `SqliteStore.getCurrentHoldings(strategyId)`(`SELECT ... WHERE strategy_id=? AND closed_at IS NULL`) **存储层**: ```sql SELECT code, shares, created_at FROM strategy_holdings WHERE strategy_id = ? AND closed_at IS NULL ``` **返回**:`[{ code, name, volume, shares, ... }]`(对外结构不变,上层 API 无感知) --- ## 操作 2:添加持仓(建仓/加仓) **触发**:策略 tab 添加表单 → 选标的 + 填份额 → 确认添加 → `handleAdd()` **前端调用**(`StrategyTab.jsx`): ```js call('one-divine-lot/add-shares', { args: { code, strategyId, shares: Number(addShares) } }); ``` **服务端** → `PositionManager.addToStrategy(code, strategyId, shares)`: - 升级前:读 code 份额 map → 校验超限 → `_setShares(code, next)`(**整策略 dataset 重写**) - 升级后:校验逻辑不变(total / 已分配 + 新增 ≤ 总持仓),写入改为**单票生命周期**: - 该策略该 code **无当前持仓** → `SqliteStore.openHolding(strategyId, code, shares)`(INSERT,created_at=now,closed_at=NULL) - **已有当前持仓** → `SqliteStore.addShares(strategyId, code, shares)`(UPDATE shares += ? WHERE closed_at IS NULL) **存储层**: ```sql -- openHolding(建仓) INSERT INTO strategy_holdings (strategy_id, code, shares, created_at, closed_at) VALUES (?, ?, ?, ?, NULL); -- addShares(加仓) UPDATE strategy_holdings SET shares = shares + ? WHERE strategy_id = ? AND code = ? AND closed_at IS NULL; ``` **返回**:`getShares(code)`(`{ strategyId: shares }`,结构不变)→ 前端 `toast.success` + `load()` 刷新 --- ## 操作 3:移出份额(减仓/清仓) **触发**:策略 tab 行内「移出」→ 填数量 → 确认 → `handleRemove()` **前端调用**(`StrategyTab.jsx`): ```js call('one-divine-lot/remove-shares', { args: { code, strategyId, shares: Number(removeTarget.shares) } }); ``` **服务端** → `PositionManager.removeFromStrategy(code, strategyId, shares)`: - 升级前:读份额 → 校验不超移 → `_setShares`(整策略重写;减到 0 则删除该 code 条目) - 升级后:校验不变,写入分两种: - 减后 **> 0** → `SqliteStore.reduceShares(strategyId, code, shares)`(UPDATE shares -= ? WHERE closed_at IS NULL) - 减到 **= 0** → `SqliteStore.closeHolding(strategyId, code)`(UPDATE closed_at=now, shares=0;**转历史,不物理删除**) **存储层**: ```sql -- reduceShares(减仓,仍持仓) UPDATE strategy_holdings SET shares = shares - ? WHERE strategy_id = ? AND code = ? AND closed_at IS NULL; -- closeHolding(清仓,转历史) UPDATE strategy_holdings SET shares = 0, closed_at = ? WHERE strategy_id = ? AND code = ? AND closed_at IS NULL; ``` **行为变化**:清仓数据**保留为历史**(closed_at 非 NULL),不再物理删除 —— 供未来交易记录/复盘关联(R-007)。 --- ## 操作 4:全部移入(单票级) **触发**:策略 tab 行内「全部移入」→ `handleMoveAllIn()` **前端调用**(`StrategyTab.jsx`): ```js call('one-divine-lot/move-all-shares', { args: { code, strategyId } }); ``` **服务端** → `PositionManager.moveAllUnallocatedToStrategy(code, strategyId)`: - 升级前:计算未分配份额 → `_setShares`(整策略重写,`current[strategyId] + unallocated`) - 升级后:等价于「把未分配份额并入该策略当前持仓」→ 走生命周期: - 无当前持仓 → `openHolding(strategyId, code, unallocated)` - 有当前持仓 → `addShares(strategyId, code, unallocated)` **存储层**:同操作 2 的 openHolding / addShares(shares = 原份额 + 未分配份额) **返回**:`getShares(code)` → `toast.success` + `load()` --- ## 操作 5:一键清零(策略级) **触发**:策略 tab 顶部「一键清零」→ 弹窗确认 → `handleClearAll()` **前端调用**(`StrategyTab.jsx`): ```js call('one-divine-lot/remove-all-shares', { args: { strategyId } }); ``` **服务端** → `PositionManager.clearStrategyShares(strategyId)`: - 升级前:`storage.getDataset(strategyId)`(读整策略)→ `storage.removeDataset(strategyId)`(**物理删除整策略 dataset**) - 升级后:**批量 closeHolding** —— 该策略所有当前持仓转历史(不物理删除): - 读 `getCurrentHoldings(strategyId)` 得到受影响 code 列表 - 逐笔 `closeHolding(strategyId, code)` - 返回受影响 code 列表(对外结构不变) **存储层**: ```sql UPDATE strategy_holdings SET shares = 0, closed_at = ? WHERE strategy_id = ? AND closed_at IS NULL; ``` **返回**:`string[]`(受影响标的 code 列表)→ 前端提示「已清空策略份额(N 只标的回到未分配)」+ `load()` --- ## 操作 6:删除策略(设置页联动清份额) **触发**:设置页「策略分组」→ 删除策略 → 弹窗确认 → `handleDelete()` **前端调用**(`SettingsSection.jsx`): ```js call('one-divine-lot/strategies/remove', { args: { strategyId: id } }); ``` **服务端**(`api/strategies.js`): ```js await removeStrategy(settings, args.strategyId); // settings 删除策略定义 await manager.clearStrategyShares(args.strategyId); // 联动清份额 return getStrategies(settings); ``` **升级后**:`clearStrategyShares` 同样走**批量 closeHolding** —— 该策略持仓全部转历史(closed_at=now),**历史保留**(供交易记录关联),settings 中策略定义删除(策略 id 从配置消失,但 SQLite 中历史行仍带 strategy_id,可追溯)。 **存储层**:同操作 5 的批量 closeHolding。 --- ## 操作 7:行情现价展示(读缓存) **触发**:任意持仓表格渲染现价列 → `useMarket().getPrice(code)` **前端调用**(`MarketDataProvider` 轮询): ```js // 表格上报 code → Provider 调行情接口(去重,不再自拉 positions) registerCodes(codes); // 周期轮询 call('one-divine-lot/market-snapshot', { args: { codes } }); ``` **服务端**(`api/market.js`)→ `MarketDataHub.getByCodes()`: - 内存缓存 → 磁盘缓存 → REST 补拉(三级命中) - 升级后:磁盘缓存由 `store.market.json` 改为 `SqliteStore.getMarketQuotes(codes)`(读 market_quotes_cache 表) **存储层**: ```sql SELECT code, last_price, last_close FROM market_quotes_cache WHERE code IN (...); ``` **返回**:`{ code: { lastPrice, lastClose } }` → PriceCell 渲染现价 + 红涨绿跌着色 --- ## 操作 8:行情写回缓存(服务端后台) **触发**:MarketDataHub.ingest() 防抖 3s 写回(非用户直接操作) **前端**:无直接调用(服务端后台行为) **服务端**:`MarketDataHub` → 升级后 `SqliteStore.setMarketQuotes(quotes)`: ```sql INSERT INTO market_quotes_cache (code, last_price, last_close, updated_at) VALUES (?, ?, ?, ?) ON CONFLICT(code) DO UPDATE SET last_price = excluded.last_price, last_close = excluded.last_close, updated_at = excluded.updated_at; ``` **说明**:只落盘 lastPrice/lastClose 两列(UI 消费的现价 + 涨跌基准);盘口五档等仅内存缓存,不落盘(方案明确)。 --- ## 存储层接口映射总表 | 前端操作 | /odl/api 端点 | PositionManager 方法 | SqliteStore 方法(升级) | 原 DataStore 方法(废弃) | |---|---|---|---|---| | 策略 tab 加载 | strategy-positions | getStrategyPositions | **getCurrentHoldings** | getDataset | | 添加(无持仓) | add-shares | addToStrategy | **openHolding** | setDataset(整策略重写) | | 添加(有持仓) | add-shares | addToStrategy | **addShares** | setDataset | | 移出(>0) | remove-shares | removeFromStrategy | **reduceShares** | setDataset | | 移出(=0) | remove-shares | removeFromStrategy | **closeHolding** | setDataset(删条目) | | 全部移入 | move-all-shares | moveAllUnallocatedToStrategy | openHolding / addShares | setDataset | | 一键清零 | remove-all-shares | clearStrategyShares | **批量 closeHolding** | removeDataset(物理删) | | 删除策略 | strategies/remove | removeStrategy + clearStrategyShares | 批量 closeHolding | removeDataset | | 行情读取 | market-snapshot | MarketDataHub.getByCodes | **getMarketQuotes** | store.market.json 读 | | 行情写回 | (后台) | MarketDataHub.ingest | **setMarketQuotes** | store.market.json 写 | ## 对前端的结论 1. **前端零改动**:所有操作仍走 /odl/api/* 端点,`useRpc` 封装、请求/响应结构不变; 2. **行为增强(前端无感)**:清仓/一键清零/删除策略后数据**转历史保留**(closed_at),而非物理删除 —— 为 R-007 交易记录关联与复盘铺路; 3. **行情落盘降维**:只落 lastPrice/lastClose 两列(响应无需关心,服务端投影); 4. **唯一可见差异**:历史持仓数据存在(未来若做「持仓历史」UI,getHoldingHistory 可直接支撑)。