- 新增 02-计划/计划-数据存储SQLite.md(PLAN-007) - 新增 04-迭代记录/06-数据存储SQLite/ 五份文档(迭代目标/技术实现方案/验收标准/UI交互调用分析/迭代复盘) - 归档 R-008 至 已完成/(含归档头 + 讨论记录索引),索引更新为已实现(已归档) - 设计约束更新:技术方案约束-012 标注 SQLite 变更、数据存储设计.md 第 9 节变更预告 - 需求池说明.md 新增「归档与转正规范」;迭代记录说明.md 新增「实现经验沉淀」 - 验收标准修正 db 文件名笔误(one-divine-lot.db → store.db)
11 KiB
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(改造)── 委托 ──▶ SqliteStore(node:sqlite,新增)
│ ├─ strategy_holdings 表(持仓生命周期)
│ └─ market_quotes_cache 表(行情快照)
关键结论:前端组件与 /odl/api/* 端点零改动;改动集中在服务端业务层(PositionManager 适配单票生命周期)+ 存储层(DataStore→SqliteStore)。以下逐操作分析。
操作 1:策略持仓 tab 加载(读)
触发:进入策略 tab → StrategyTab.load()
前端调用(StrategyTab.jsx):
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)
存储层:
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):
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)
- 该策略该 code 无当前持仓 →
存储层:
-- 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):
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;转历史,不物理删除)
- 减后 > 0 →
存储层:
-- 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):
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):
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 列表(对外结构不变)
- 读
存储层:
UPDATE strategy_holdings SET shares = 0, closed_at = ?
WHERE strategy_id = ? AND closed_at IS NULL;
返回:string[](受影响标的 code 列表)→ 前端提示「已清空策略份额(N 只标的回到未分配)」+ load()
操作 6:删除策略(设置页联动清份额)
触发:设置页「策略分组」→ 删除策略 → 弹窗确认 → handleDelete()
前端调用(SettingsSection.jsx):
call('one-divine-lot/strategies/remove', { args: { strategyId: id } });
服务端(api/strategies.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 轮询):
// 表格上报 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 表)
存储层:
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):
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 写 |
对前端的结论
- 前端零改动:所有操作仍走 /odl/api/* 端点,
useRpc封装、请求/响应结构不变; - 行为增强(前端无感):清仓/一键清零/删除策略后数据转历史保留(closed_at),而非物理删除 —— 为 R-007 交易记录关联与复盘铺路;
- 行情落盘降维:只落 lastPrice/lastClose 两列(响应无需关心,服务端投影);
- 唯一可见差异:历史持仓数据存在(未来若做「持仓历史」UI,getHoldingHistory 可直接支撑)。