feat: 迭代03-05 交易记录接入、行情实时、QMT连接配置、data store 标准化
- R-004 QMT连接配置:多配置管理 + 会话头部快捷切换(03-QMT连接配置) - R-005 WS盘中价格实时更新:服务端中转+缓存+前端轮询(04-WS盘中价格实时更新) - R-006 策略数据 JSON data store 标准化(store.json/market.json/schema.json) - R-007 交易记录接入:当日订单+成交单表合并(委托主行+展开成交明细)、时间段查询(今日/本周/本月+手动起止)、费用计算(佣金费率万m_dCommission+最低5元、印花税卖出万5、过户费双向万0.1) - 架构治理:api 按领域拆分、component 目录、QmtHealthMonitor
This commit is contained in:
+161
@@ -37,6 +37,17 @@ export const strategySchema = z.object({
|
||||
tradeRecords: true,
|
||||
watchlist: true,
|
||||
}),
|
||||
// QMT 连接配置(R-004,技术约束-008):list + 激活态 + 默认态
|
||||
qmtConnections: z.object({
|
||||
list: z.array(z.object({
|
||||
id: z.string().required(),
|
||||
name: z.string().required(),
|
||||
baseUrl: z.string().required(),
|
||||
order: z.number().default(0),
|
||||
})).default([]),
|
||||
activeId: z.string().default(''),
|
||||
defaultId: z.string().default(''),
|
||||
}).default({ list: [], activeId: '', defaultId: '' }),
|
||||
});
|
||||
|
||||
/** 默认通用 tab 配置 */
|
||||
@@ -308,3 +319,153 @@ export async function updateTabConfig(scope, tabs) {
|
||||
};
|
||||
await scope.update(next);
|
||||
}
|
||||
|
||||
/**
|
||||
* ============================================================================
|
||||
* QMT 连接配置(R-004 已定稿;产品约束-005、技术约束-008)
|
||||
*
|
||||
* 数据结构:qmtConnections: { list: [{id,name,baseUrl,order}], activeId, defaultId }
|
||||
* - 激活:一次只能激活一个(activeId),激活配置为当前生效连接,切换立即生效(Q2)
|
||||
* - 默认:启动时自动激活默认配置(Q1),重启回到默认;运行中手动激活立即生效
|
||||
* - 启动兜底:列表为空时回退 cordis 注入的 qmtBaseUrl(Q4 不迁移)
|
||||
* - 删除边界(Q6):删激活→自动切默认;删默认→默认转移第一条并激活之;禁删最后一条
|
||||
*/
|
||||
|
||||
/** 归一化并校验 HTTP 地址:trim + 去尾斜杠 + 校验 http(s):// 前缀 */
|
||||
export function normalizeBaseUrl(raw) {
|
||||
const url = String(raw ?? '').trim().replace(/\/+$/, '');
|
||||
if (!/^https?:\/\//i.test(url) || url.length <= 8) {
|
||||
throw Object.assign(new Error('HTTP 地址格式无效(需以 http:// 或 https:// 开头)'), { code: 'qmt-invalid-url' });
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
/** 生成连接配置英文 id(复用策略拼音 slug;兜底前缀 qmt-conn) */
|
||||
export function generateQmtConnectionId(name, existingIds = []) {
|
||||
const base = generateStrategyId(name, []);
|
||||
const slug = base && base !== 'strategy' ? base : 'qmt-conn';
|
||||
return dedupe(slug, existingIds);
|
||||
}
|
||||
|
||||
/** 读取 QMT 连接配置(list 按 order 排序) */
|
||||
export function getQmtConnections(scope) {
|
||||
const value = scope?.get() ?? {};
|
||||
const qc = value.qmtConnections ?? {};
|
||||
const list = Array.isArray(qc.list) ? qc.list : [];
|
||||
return {
|
||||
list: list.slice().sort((a, b) => (a.order ?? 0) - (b.order ?? 0)),
|
||||
activeId: typeof qc.activeId === 'string' ? qc.activeId : '',
|
||||
defaultId: typeof qc.defaultId === 'string' ? qc.defaultId : '',
|
||||
};
|
||||
}
|
||||
|
||||
/** 解析当前生效连接:activeId → defaultId → order 第一条 → null */
|
||||
export function getActiveQmtConnection(state) {
|
||||
const byId = (id) => (id ? state.list.find((c) => c.id === id) ?? null : null);
|
||||
return byId(state.activeId) ?? byId(state.defaultId) ?? state.list[0] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动激活连接解析(Q1/Q4):默认 → 上次激活 → 第一条;列表为空回退 cordis qmtBaseUrl。
|
||||
* 解析结果与 activeId 不一致时回写(保证运行期状态一致)。
|
||||
* @returns {Promise<{connection: object|null, baseUrl: string|null, source: string}>}
|
||||
*/
|
||||
export async function resolveStartupConnection(scope, config) {
|
||||
const state = getQmtConnections(scope);
|
||||
if (!state.list.length) {
|
||||
const fallback = String(config?.qmtBaseUrl ?? '').trim().replace(/\/+$/, '');
|
||||
return { connection: null, baseUrl: fallback || null, source: fallback ? 'cordis-fallback' : 'none' };
|
||||
}
|
||||
const byId = (id) => (id ? state.list.find((c) => c.id === id) ?? null : null);
|
||||
const defaultConn = byId(state.defaultId);
|
||||
const activeConn = byId(state.activeId);
|
||||
const chosen = defaultConn
|
||||
? { conn: defaultConn, why: 'default' }
|
||||
: activeConn
|
||||
? { conn: activeConn, why: 'last-active' }
|
||||
: { conn: state.list[0], why: 'first' };
|
||||
if (chosen.conn.id !== state.activeId) {
|
||||
await activateQmtConnection(scope, chosen.conn.id);
|
||||
}
|
||||
return { connection: chosen.conn, baseUrl: chosen.conn.baseUrl, source: 'settings:' + chosen.why };
|
||||
}
|
||||
|
||||
/** 新增连接配置 */
|
||||
export async function addQmtConnection(scope, { name, baseUrl } = {}) {
|
||||
const trimmedName = String(name ?? '').trim();
|
||||
if (!trimmedName) throw Object.assign(new Error('连接名称不能为空'), { code: 'qmt-name-required' });
|
||||
const normalizedUrl = normalizeBaseUrl(baseUrl);
|
||||
const state = getQmtConnections(scope);
|
||||
const id = generateQmtConnectionId(trimmedName, state.list.map((c) => c.id));
|
||||
const conn = {
|
||||
id,
|
||||
name: trimmedName,
|
||||
baseUrl: normalizedUrl,
|
||||
order: state.list.reduce((max, c) => Math.max(max, c.order ?? 0), -1) + 1,
|
||||
};
|
||||
const value = scope?.get() ?? {};
|
||||
await scope.update({ ...value, qmtConnections: { ...state, list: [...state.list, conn] } });
|
||||
return conn;
|
||||
}
|
||||
|
||||
/** 编辑连接配置(名称 / 地址);返回更新后的配置对象 */
|
||||
export async function updateQmtConnection(scope, id, patch = {}) {
|
||||
const state = getQmtConnections(scope);
|
||||
const target = state.list.find((c) => c.id === id);
|
||||
if (!target) throw Object.assign(new Error('连接配置不存在: ' + id), { code: 'qmt-connection-not-found' });
|
||||
const nextName = patch.name !== undefined ? String(patch.name).trim() : target.name;
|
||||
if (!nextName) throw Object.assign(new Error('连接名称不能为空'), { code: 'qmt-name-required' });
|
||||
const nextUrl = patch.baseUrl !== undefined ? normalizeBaseUrl(patch.baseUrl) : target.baseUrl;
|
||||
const updated = { ...target, name: nextName, baseUrl: nextUrl };
|
||||
const value = scope?.get() ?? {};
|
||||
await scope.update({
|
||||
...value,
|
||||
qmtConnections: { ...state, list: state.list.map((c) => (c.id === id ? updated : c)) },
|
||||
});
|
||||
return updated;
|
||||
}
|
||||
|
||||
/** 删除连接配置(含 Q6 删除边界处理);返回删除后的状态与边界信息 */
|
||||
export async function removeQmtConnection(scope, id) {
|
||||
const state = getQmtConnections(scope);
|
||||
if (!state.list.some((c) => c.id === id)) {
|
||||
throw Object.assign(new Error('连接配置不存在: ' + id), { code: 'qmt-connection-not-found' });
|
||||
}
|
||||
if (state.list.length <= 1) {
|
||||
throw Object.assign(new Error('不能删除最后一个连接配置'), { code: 'qmt-last-connection' });
|
||||
}
|
||||
const removedWasActive = state.activeId === id;
|
||||
const removedWasDefault = state.defaultId === id;
|
||||
const list = state.list.filter((c) => c.id !== id);
|
||||
// 删默认 → 默认标记转移到列表第一条(产品约束-005)
|
||||
let defaultId = removedWasDefault ? (list[0]?.id ?? '') : state.defaultId;
|
||||
// 删激活 → 自动切换到默认(若默认仍有效),否则第一条
|
||||
let activeId = removedWasActive
|
||||
? (defaultId && list.some((c) => c.id === defaultId) ? defaultId : (list[0]?.id ?? ''))
|
||||
: state.activeId;
|
||||
const value = scope?.get() ?? {};
|
||||
await scope.update({ ...value, qmtConnections: { list, activeId, defaultId } });
|
||||
return { list, activeId, defaultId, removedWasActive, removedWasDefault };
|
||||
}
|
||||
|
||||
/** 激活连接配置(写 activeId;热切换由 api 层编排 setBaseUrl) */
|
||||
export async function activateQmtConnection(scope, id) {
|
||||
const state = getQmtConnections(scope);
|
||||
if (!state.list.some((c) => c.id === id)) {
|
||||
throw Object.assign(new Error('连接配置不存在: ' + id), { code: 'qmt-connection-not-found' });
|
||||
}
|
||||
const value = scope?.get() ?? {};
|
||||
await scope.update({ ...value, qmtConnections: { ...state, activeId: id } });
|
||||
return { ...state, activeId: id };
|
||||
}
|
||||
|
||||
/** 设为默认配置(写 defaultId) */
|
||||
export async function setDefaultQmtConnection(scope, id) {
|
||||
const state = getQmtConnections(scope);
|
||||
if (!state.list.some((c) => c.id === id)) {
|
||||
throw Object.assign(new Error('连接配置不存在: ' + id), { code: 'qmt-connection-not-found' });
|
||||
}
|
||||
const value = scope?.get() ?? {};
|
||||
await scope.update({ ...value, qmtConnections: { ...state, defaultId: id } });
|
||||
return { ...state, defaultId: id };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user