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:
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* 服务端 API:QMT 连接配置域(从 api.js 拆分,2026-08-31)
|
||||
*
|
||||
* 端点:
|
||||
* qmt-connections / add / update / remove / activate / set-default / test
|
||||
* active-host
|
||||
*
|
||||
* 热切换编排:激活/更新/删除连接导致生效地址变化时,同步 dataSource.setBaseUrl + marketFeed.setBaseUrl
|
||||
* (技术约束-008:按请求读取 baseUrl,改字段即生效)
|
||||
*
|
||||
* 2026-08-31:移除 subscribe-ws / unsubscribe-ws / tick-snapshot(架构演进后无人调用,
|
||||
* 订阅与快照统一由 MarketFeed/MarketDataHub 内部处理,不再暴露为 HTTP 端点)
|
||||
*/
|
||||
|
||||
import {
|
||||
getQmtConnections, getActiveQmtConnection,
|
||||
addQmtConnection, updateQmtConnection, removeQmtConnection,
|
||||
activateQmtConnection, setDefaultQmtConnection,
|
||||
} from '../settings.js';
|
||||
|
||||
/** QMT 连接端点方法表 */
|
||||
export const QMT_METHODS = new Set([
|
||||
'qmt-connections',
|
||||
'qmt-connections/add',
|
||||
'qmt-connections/update',
|
||||
'qmt-connections/remove',
|
||||
'qmt-connections/activate',
|
||||
'qmt-connections/set-default',
|
||||
'qmt-connections/test',
|
||||
'qmt-connections/active-host',
|
||||
]);
|
||||
|
||||
/** 服务端代理测试连接:GET {baseUrl}/health(浏览器跨域规避,Q3 手动测试) */
|
||||
async function testQmtConnection(ctx, args) {
|
||||
const baseUrl = String(args?.baseUrl ?? '').trim().replace(/\/+$/, '');
|
||||
if (!/^https?:\/\//i.test(baseUrl)) {
|
||||
throw Object.assign(new Error('HTTP 地址格式无效(需以 http:// 或 https:// 开头)'), { code: 'qmt-invalid-url' });
|
||||
}
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), 15000);
|
||||
const startedAt = Date.now();
|
||||
try {
|
||||
const res = await fetch(baseUrl + '/health', { signal: controller.signal });
|
||||
const latencyMs = Date.now() - startedAt;
|
||||
let status = null;
|
||||
try { status = (await res.json())?.status ?? null; } catch { /* 非 JSON 响应按 status=null */ }
|
||||
return { ok: res.ok, latencyMs, httpStatus: res.status, status, reachable: res.ok };
|
||||
} catch (err) {
|
||||
return {
|
||||
ok: false,
|
||||
latencyMs: Date.now() - startedAt,
|
||||
reachable: false,
|
||||
error: err.name === 'AbortError' ? '请求超时(15s)' : (err?.message ?? String(err)),
|
||||
};
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 QMT 连接端点
|
||||
* @param {string} method
|
||||
* @param {object} args
|
||||
* @param {object} runtime { manager, settings, dataSource, marketCache }
|
||||
*/
|
||||
export async function handleQmt(method, args, { ctx, settings, dataSource, marketFeed, qmtHealthMonitor }) {
|
||||
switch (method) {
|
||||
case 'qmt-connections': {
|
||||
const state = getQmtConnections(settings);
|
||||
return { ...state, active: getActiveQmtConnection(state) };
|
||||
}
|
||||
case 'qmt-connections/add':
|
||||
return await addQmtConnection(settings, { name: args.name, baseUrl: args.baseUrl });
|
||||
case 'qmt-connections/update': {
|
||||
const before = getQmtConnections(settings);
|
||||
const value = await updateQmtConnection(settings, args.id, { name: args.name, baseUrl: args.baseUrl });
|
||||
// 编辑的是激活配置且地址变化 → 立即热切换(技术约束-008)
|
||||
if (before.activeId === args.id && value.baseUrl !== dataSource.baseUrl) {
|
||||
dataSource.setBaseUrl(value.baseUrl);
|
||||
marketFeed?.setBaseUrl(value.baseUrl);
|
||||
qmtHealthMonitor?.probe(); // 切换后立即补探健康
|
||||
ctx?.logger?.info('[one-divine-lot] 激活配置地址已变更,数据源热切换 → ' + value.baseUrl);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
case 'qmt-connections/remove': {
|
||||
const result = await removeQmtConnection(settings, args.id);
|
||||
// 生效连接因删除发生变化 → 立即热切换(产品约束-005 删除边界)
|
||||
const afterState = getQmtConnections(settings);
|
||||
const active = getActiveQmtConnection(afterState);
|
||||
if (active && active.baseUrl !== dataSource.baseUrl) {
|
||||
dataSource.setBaseUrl(active.baseUrl);
|
||||
marketFeed?.setBaseUrl(active.baseUrl);
|
||||
qmtHealthMonitor?.probe(); // 切换后立即补探健康
|
||||
ctx?.logger?.info('[one-divine-lot] 激活配置已删除,自动切换 → ' + active.name + ' (' + active.baseUrl + ')');
|
||||
}
|
||||
return { ...afterState, active, removedWasActive: result.removedWasActive, removedWasDefault: result.removedWasDefault };
|
||||
}
|
||||
case 'qmt-connections/activate': {
|
||||
const state = await activateQmtConnection(settings, args.id);
|
||||
const active = getActiveQmtConnection(state);
|
||||
if (active && active.baseUrl !== dataSource.baseUrl) {
|
||||
dataSource.setBaseUrl(active.baseUrl);
|
||||
marketFeed?.setBaseUrl(active.baseUrl);
|
||||
qmtHealthMonitor?.probe(); // 切换后立即补探健康
|
||||
ctx?.logger?.info('[one-divine-lot] QMT 连接激活: ' + active.name + ' (' + active.baseUrl + ')');
|
||||
}
|
||||
return { ...state, active };
|
||||
}
|
||||
case 'qmt-connections/set-default':
|
||||
return await setDefaultQmtConnection(settings, args.id);
|
||||
case 'qmt-connections/test': {
|
||||
// 支持传未保存的 baseUrl(表单先测后存),或传 id 用已存配置地址
|
||||
let url = args.baseUrl;
|
||||
if (!url && args.id) {
|
||||
const state = getQmtConnections(settings);
|
||||
url = state.list.find((c) => c.id === args.id)?.baseUrl;
|
||||
}
|
||||
return await testQmtConnection(ctx, { baseUrl: url });
|
||||
}
|
||||
case 'qmt-connections/active-host': {
|
||||
// R-005:返回激活配置的 host 与 ws 地址
|
||||
const state = getQmtConnections(settings);
|
||||
const active = getActiveQmtConnection(state) ?? { baseUrl: dataSource.baseUrl, name: '(fallback)' };
|
||||
const baseUrl = String(active.baseUrl ?? '').replace(/\/+$/, '');
|
||||
let host = '';
|
||||
let wsUrl = '';
|
||||
try {
|
||||
const u = new URL(baseUrl);
|
||||
host = u.host; // 含端口,如 100.110.38.78:8610
|
||||
wsUrl = 'ws://' + u.host + '/ws';
|
||||
} catch { /* baseUrl 非法时返回空 */ }
|
||||
return { name: active.name, baseUrl, host, wsUrl, activeId: state.activeId ?? '' };
|
||||
}
|
||||
default:
|
||||
throw Object.assign(new Error('unknown qmt method: ' + method), { code: 'not-found' });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user