Files
one_divine_lot/docs/04-迭代记录/06-数据存储SQLite/UI交互调用分析.md
T
kyugao dd98b4d45b docs(迭代06): 数据存储SQLite迭代文档 + 需求归档(R-008)
- 新增 02-计划/计划-数据存储SQLite.md(PLAN-007)
- 新增 04-迭代记录/06-数据存储SQLite/ 五份文档(迭代目标/技术实现方案/验收标准/UI交互调用分析/迭代复盘)
- 归档 R-008 至 已完成/(含归档头 + 讨论记录索引),索引更新为已实现(已归档)
- 设计约束更新:技术方案约束-012 标注 SQLite 变更、数据存储设计.md 第 9 节变更预告
- 需求池说明.md 新增「归档与转正规范」;迭代记录说明.md 新增「实现经验沉淀」
- 验收标准修正 db 文件名笔误(one-divine-lot.db → store.db)
2026-09-01 18:04:28 +08:00

248 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# UI 交互调用分析:06-数据存储SQLite
> 分析日期:2026-09-01 | 依据:技术实现方案.md(接口定义)
> 目的:基于 SQLite 存储层接口(strategy_holdings / market_quotes_cache + 持仓生命周期方法集),梳理**前端各操作 → 服务端 → 存储层**的完整调用链,确认对外 API 不变的前提下,存储层升级对调用流程的影响。
## 调用链路总览(升级后不变的部分)
```
前端组件(React
│ fetch POST /odl/api/<method> body={ args }
connection.jsx useRpc() ── 标准化剥离 one-divine-lot/ 前缀
服务端 src/api/*.js 领域分发(positions / strategies / qmt-connections / market / trades
PositionManager / MarketDataHub / Settings(业务逻辑)
DataStore(改造)── 委托 ──▶ SqliteStorenode: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)`INSERTcreated_at=nowclosed_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 / addSharesshares = 原份额 + 未分配份额)
**返回**`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 可直接支撑)。