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)
This commit is contained in:
2026-09-01 18:04:28 +08:00
parent 9b719f13ba
commit dd98b4d45b
12 changed files with 782 additions and 3 deletions
@@ -0,0 +1,247 @@
# 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 可直接支撑)。