feat(R-013+重构): 策略自定义字段需求文档 + 结构归类优化

需求与文档:
- R-013 策略自定义字段配置(定义随策略 configSchema 存 settings、值落
  strategy_holdings.values JSON、类型化文本/数字/布尔/枚举)定稿并进入
  PLAN-012 / 迭代 11(含目标数据模型 + 端到端示例 + 技术方案 + 验收标准)
- R-011(Tab设置)/ R-012(UI主题适配)归档至 已完成/,索引标记已归档
- 沉淀约束: 产品约束-010 / 技术约束-015 / 技术约束-016 / UI约束-005

结构重构(技术约束-016):
- src/component/ 平铺还原为语义分域目录: data-source/ storage/ position/
  market/ trades/(git rename 保留历史)
- 清理死代码: 删除 AllocationStorage.js(0 引用)、DataStore
  setDataset/removeDataset(无调用方)
- 同步更新 src/index.js / scripts/*.mjs / tsdown.config.ts 引用
- typecheck + build + test-r009 回归 14/14 通过
This commit is contained in:
2026-09-02 15:38:51 +08:00
parent f1e7e785a1
commit a4902bb730
31 changed files with 509 additions and 198 deletions
@@ -0,0 +1,89 @@
# 技术实现方案:11-策略自定义字段配置
> 迭代编号:11 依据:PLAN-012 + R-013(目标数据模型 / 端到端示例)+ 技术约束-015 / 产品约束-010 / UI约束-005
## 1. 数据层
### 1.1 settings.strategies 扩展 configSchemasrc/settings.js
```js
// strategySchema 中 strategies 项扩展:
strategies: z.array(z.object({
id: z.string().required(),
name: z.string().required(),
configSchema: z.array(z.object({
key: z.string().required(), // 稳健标识(英文 slug
label: z.string(), // UI 展示名(可中文)
type: z.union([z.const('text'), z.const('number'), z.const('boolean'), z.const('enum')]).required(),
enum: z.array(z.string()), // 仅 type=enum 时使用
def: z.any(), // 默认值(text=string / number=number / boolean=boolean / enum=枚举项)
})).default([]), // 缺省 []
})).default(DEFAULT_STRATEGIES),
```
- 读取归一化:旧项无 configSchema → 补默认空数组(读取时或 schema default 保证);
- addStrategy 返回值默认带 configSchema: []renameStrategy / updateStrategies 整表更新天然携带;
- 兼容:settings 旧数据({id,name})经 schemastery default/union 归一化,无迁移脚本。
### 1.2 strategy_holdings 新增 values 列 + 读写(src/component/SqliteStore.js
```js
// init() 内补列(幂等,沿用 _ensureTradeAttributionColumns 模式;只读连接容忍)
_ensureHoldingValuesColumn() {
const cols = new Set(this.db.prepare('PRAGMA table_info(strategy_holdings)').all().map(c => c.name));
if (!cols.has('values')) this.db.exec('ALTER TABLE strategy_holdings ADD COLUMN values TEXT');
}
readValues(holdingId) // SELECT values FROM strategy_holdings WHERE holding_id=? → JSON.parse ?? null
writeValues(holdingId, values) // UPDATE strategy_holdings SET values=? WHERE holding_id=? → JSON.stringify
```
- openHolding/addShares/reduceShares/closeHolding **不碰 values 列**(份额生命周期与字段值独立);
- getCurrentHoldings / getHoldingHistory 的 _mapHolding 附加 values 字段(解析 JSON)。
## 2. API 层(src/api/strategies.js
```js
// 既有:strategy-positions → manager.getStrategyPositions(strategyId)
// PositionManager 返回项附加 valuesfrom SqliteStore 映射)
// 新增端点:
case 'holdings/values-update':
return await manager.updateHoldingValues(args.holdingId, args.values);
```
```js
// PositionManager.updateHoldingValues(holdingId, values)
// 1) 定位 holdings 行(须存在且当前持仓 closed_at IS NULL;否则 404
// 2) 服务端校验(按所属策略 configSchema):
// number → Number.isFinite(Number(v))enum → enum.includes(v)boolean → typeof v === 'boolean'
// 未定义 key 的额外键放行(可扩展);空值/缺省可写入
// 3) 校验失败抛 { code: 'field-validation' },成功 writeValues 并返回 { holdingId, values }
```
## 3. 设置页 UIsrc/client/views/SettingsSection.jsx
- 「策略分组」子 tab 策略行加展开手柄(▶/▸ toggle,样式沿用 expand 惯例);
- 展开区:
- 字段列表表格:展示名 | key | 类型 | 枚举选项 | 默认值 | 操作(删除)—— 空态提示「尚未配置自定义字段」;
- 添加/编辑字段表单:字段名(label)→ 自动生成 key(拼音 slug,复用 generateStrategyId 思路)/ 可手改校验唯一、类型下拉(文本/数字/布尔/枚举)、枚举选项(type=enum 时逗号分隔输入)、默认值(按类型渲染输入);
- 行内编辑/删除:编辑回填表单,删除后保存生效;
- 保存:整表提交 strategies/update(映射为 { ...s, configSchema }),成功 Toast + 刷新(沿用「刷新页面后生效」机制)。
## 4. 策略持仓 tab UIsrc/client/views/StrategyTab.jsx
- 持仓行展开区(R-010 展开逻辑基础上)增加「自定义字段」区块(configSchema 非空才渲染,空则无此区块);
- 块内按 configSchema 渲染控件:text=输入框 / number=数字输入 / boolean=开关(复用 Switch 组件)/ enum=下拉;
- 值来源:strategy-positions 返回的 values;控件初值 = values[key] ?? def
- 编辑即保存:单个字段变更(或区块「保存」按钮)→ 调 one-divine-lot/holdings/values-update;成功 Toast;校验失败(服务端返回)Toast 错误并回滚输入;
- 与交易记录展开区块并存(展开区纵向分区:交易记录 / 自定义字段)。
## 5. 回归脚本(scripts/test-r013-custom-fields.mjs
- ODL_TEST_DATA_DIR 独立数据目录(技术约束-011);
- 用例:addStrategy 带 configSchema(四类型)→ openHolding ×2 同策略不同 code → writeValues 各自值 → 读回校验 → 校验拦截(数字非法 / 枚举越界)→ 旧策略无 configSchema 持仓行 readValues 为 NULL
- 断言存储与读取一致、校验拒绝。
## 6. 验证
- build + typecheck 通过;
- 老师人工验收(见验收标准)。
@@ -0,0 +1,66 @@
# 程序结构设计:11-策略自定义字段配置(含结构归类优化)
> 迭代编号:11 | 2026-09-02 | 依据:技术约束-016(目录归类)、R-013、PLAN-012
## 背景:结构审查发现与优化
2026-09-02 结构审查发现:src/component/ 平铺 10 个服务端模块,偏离迭代 01/02 程序结构设计的语义分域(data-source/、position/、storage/);`component` 命名语义含混(UI 组件实际在 client/views/);归类演进无文档记录;存在死代码。
## 优化动作(2026-09-02 落地)
1. **归类还原**component/ 平铺 → 按功能域分目录:
```
src/
├── index.js # 插件入口(组装各模块)
├── settings.js # 设置管理(策略 + tabs + QMT 连接配置)
├── api/ # 服务端 HTTP APIwebServer /odl/api/*),按领域拆子文件
│ ├── index.js # 路由入口(合并各领域 + 405/404/500
│ ├── common.js # 公共工具
│ ├── positions.js # 持仓域
│ ├── strategies.js # 策略/份额/tabs 域
│ ├── qmt-connections.js # QMT 连接配置域
│ ├── market.js # 行情域
│ └── trades.js # 交易记录域
├── data-source/ # 数据源抽象层(技术约束-001/003)
│ ├── QmtBridgeRestDataSource.js # QMT Bridge REST 适配器
│ ├── data-source-types.js # 统一数据模型(Position/DataSource 接口)
│ └── QmtHealthMonitor.js # QMT 连接健康检查(数据源健康缓存)
├── storage/ # 存储层(R-008 SQLite
│ ├── SqliteStore.js # SQLite 存储封装(node:sqlite
│ └── DataStore.js # 存储门面(对外兼容 API + 迁移)
├── position/ # 分仓业务逻辑
│ └── PositionManager.js # 持仓↔策略份额分配、聚合视图
├── market/ # 行情域
│ ├── MarketDataHub.js # 行情缓存(内存 + 落盘 + 查询)
│ └── MarketFeed.js # 行情获取(WS + REST 轮询 + prime
├── trades/ # 交易域
│ └── TradeSync.js # 交易记录定时同步(QMT → SQLite)
└── client/ # 客户端(UI)
├── index.js # 客户端入口:tab/settings 注册
├── market/ # MarketDataProvider.jsx(行情 context
└── views/ # React 组件(StrategyTab / SettingsSection / Toast 等)
```
2. **死代码清理**:删除 src/component/AllocationStorage.js(旧 allocations.json 存储,0 引用);删除 DataStore.setDataset/removeDatasetPositionManager 适配单票生命周期后无调用方);
3. **构建与脚本同步**tsdown.config.ts entry 更新为新目录 globscripts/*.mjs 导入路径更新;
4. **验证**typecheck + build 通过;test-r009 回归 14/14 通过(独立数据目录)。
## 归类规则(沉淀为技术约束-016)
- 服务端代码禁止平铺,按功能域分目录:data-source / storage / position / market / trades
- api/ 按领域拆子文件;client/ 仅放 UIviews/ 组件 + market/ provider);
- 文件命名 = 类名 PascalCase + .js/.jsx
- 新增服务端模块必须先落对应域目录,无合适域时先讨论补域,不得回退平铺。
## R-013 新增改动落点(本迭代实施)
```
src/settings.js # +configSchema(策略 schema 扩展)
src/storage/SqliteStore.js # +_ensureHoldingValuesColumn + readValues/writeValues
src/position/PositionManager.js # +updateHoldingValues(按 configSchema 校验)
src/api/strategies.js # +holdings/values-update 端点;strategy-positions 附加 values
src/client/views/SettingsSection.jsx # 「策略分组」策略行展开配置字段
src/client/views/StrategyTab.jsx # 持仓行展开区「自定义字段」编辑
scripts/test-r013-custom-fields.mjs # 回归脚本(独立数据目录)
```
@@ -0,0 +1,21 @@
# 迭代目标:11-策略自定义字段配置(定义随策略,值落库)
> 迭代编号:11 | 创建:2026-09-02 状态:进行中
> 依据计划:PLAN-012 需求:R-013(已定稿,2026-09-02,老师确认 Q1-Q4 + D6
## 目标描述
给策略增加**可自定义、可扩展**的字段能力:每个策略在设置页「策略分组」子 tab 配置自定义字段定义(configSchemakey/label/type/enum/默认值,随策略定义存 settings 不落库);该策略下每个持仓(strategy_holdings 行)按所属策略的定义存取一份键值对值(新增 values JSON 列落库);策略持仓 tab 持仓行展开区按定义渲染并编辑值。旧策略(无定义)行为与现状完全一致。
## 目标分解
1. 数据层:settings.strategies 扩展 configSchemaschemasterytype union text|number|boolean|enum),读取归一化(旧项缺省 []);strategy_holdings 新增 values TEXT 列(幂等 ALTER+ readValues/writeValues
2. API 层:strategy-positions 每行附 values;新增 holdings/values-update {holdingId, values} 写回(服务端按 configSchema 校验:数字/枚举/布尔);
3. 设置页 UI:「策略分组」子 tab 策略行可展开 → 字段列表 + 添加/编辑/删除字段表单 + 保存;
4. 策略持仓 UI:持仓行展开区(R-010 基础上)增加「自定义字段」区块,按定义渲染四种类型控件,编辑即保存;
5. 回归脚本(独立数据目录,技术约束-011)+ 验证 + 验收复核。
## 对老师的配合需求
- 验收:设置页配置字段定义(四种类型各一 + 枚举)→ 策略持仓 tab 持仓行展开编辑值 → 重启验证持久化;
- 提供:是否接受「旧策略无定义」行为样(默认接受,Q4 已确认)。
@@ -0,0 +1,22 @@
# 验收标准:11-策略自定义字段配置
> 迭代编号:11 | 依据:PLAN-012 验收要点 + R-013
## 验收标准线
1. 设置页「策略分组」:策略行可展开,添加 / 编辑 / 删除字段(四种类型:文本/数字/布尔/枚举,含枚举选项与默认值)保存后生效,重启后定义仍在;
2. 策略持仓 tab:持仓行展开区出现「自定义字段」区块(仅该策略配置了字段时),四种类型控件按定义渲染(文本=输入框、数字=数字输入、布尔=开关、枚举=下拉),编辑值保存后刷新仍在(已落库);
3. 同策略多行各有各的值(600719 与 300057 互不影响);不同策略字段集互不影响;
4. 旧策略(无 configSchema):持仓行不渲染字段区,现有操作(加仓/减仓/清仓/展开交易记录)与现状完全一致;
5. 服务端校验生效:数字填非数字、枚举填选项外 → 拒绝保存并 Toast 提示;空值/缺省允许;
6. 兼容迁移:既有 strategy_holdings 数据行(无 values)补列后为 NULL,读取正常,不报错;
7. build + typecheck 通过;回归脚本(test-r013-custom-fields.mjs,独立数据目录)全绿。
## 验收方法
- 回归脚本跑通(隔离数据目录);
- 老师人工验收:设置页配置网格超市 4 字段 → 策略持仓 tab 两个持仓行展开分别编辑值 → 另配长线持有 2 字段观察互不影响 → 手动做T(旧策略)确认无字段区 → 重启后确认定义与值均保留。
## 验收目标
- 7 条验收线通过,迭代 11 标记「验收通过」,R-013 更新实现状态(已实现),归档。