主站 - 新增 m0006 invite_codes 迁移;注册强制邀请码(首个管理员除外),消码与建号 同一事务,并发提交只有一个能成功 - 新增 /api/hub-admin/* 服务端点(共享 HUB_ADMIN_TOKEN,先于鉴权校验),供数据 中枢桥接读写会话/密码/模型池/会员/邀请码,并提供供应商模型列表拉取 - 前端:注册表单加邀请码(桌面 login、index.html、移动端);「系统管理」改为 「数据中枢」入口指向 8766,原模型池与会员管理分区移除,仅留「行情管理」; 随之清理陈旧 CSS 数据中枢 - 取消独立账号:删除 hub_admin/hub_sessions 与登录、改密、锁定逻辑,改为校验 主站 xiaobai_session,仅管理员可进,CSRF 由会话派生,危险操作二次确认走主站 - 控制台新增数据源凭证可编辑区(原有内容一项不删)、供应商制模型池(自动拉取 /models,失败退回卡内手动录入)、会员管理与邀请码页 - 日夜双主题:颜色收敛为同名 token 换值,SVG 改用 inline style 以吃到变量 自测 - 主站 verify_baseline 通过(498 项);数据中枢 235 项通过 - tools/verify_datahub_console.py 端到端跑通两服务真实对话; tools/verify_datahub_console_ui.py 浏览器跑通门禁/凭证/模型池/会员/主题/1030 窄屏 Co-authored-by: multica-agent <github@multica.ai>
164 lines
8.9 KiB
Python
164 lines
8.9 KiB
Python
from __future__ import annotations
|
|
|
|
import copy
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
if str(ROOT) not in sys.path:
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
TRADE_DATE = "20240902"
|
|
|
|
RAW = {
|
|
"trade_cal": [
|
|
{"exchange": "SSE", "cal_date": "20240902", "is_open": 1, "pretrade_date": "20240830"},
|
|
{"exchange": "SSE", "cal_date": "20240903", "is_open": 1, "pretrade_date": "20240902"},
|
|
{"exchange": "SSE", "cal_date": "20240907", "is_open": 0, "pretrade_date": "20240906"},
|
|
],
|
|
"stock_basic": [
|
|
{"ts_code": "600000.SH", "symbol": "600000", "name": "浦发银行", "area": "上海", "industry": "银行", "market": "主板", "list_status": "L", "list_date": "19991110"},
|
|
{"ts_code": "000001.SZ", "symbol": "000001", "name": "平安银行", "area": "深圳", "industry": "银行", "market": "主板", "list_status": "L", "list_date": "19910403"},
|
|
],
|
|
"daily": [
|
|
{"ts_code": "600000.SH", "trade_date": "20240902", "open": 10.11, "high": 10.25, "low": 10.01, "close": 10.20, "pct_chg": 1.2345, "vol": 1000.0, "amount": 2000.0},
|
|
{"ts_code": "000001.SZ", "trade_date": "20240902", "open": 11.00, "high": 11.20, "low": 10.90, "close": 11.10, "pct_chg": -0.5, "vol": 2000.0, "amount": 4000.0},
|
|
],
|
|
"daily_basic": [
|
|
{"ts_code": "600000.SH", "trade_date": "20240902", "turnover_rate": 1.2, "volume_ratio": 0.8, "total_mv": 1000.0, "circ_mv": 800.0, "pe_ttm": 5.1, "pb": 0.6, "ps_ttm": 1.1, "dv_ttm": 4.0},
|
|
{"ts_code": "000001.SZ", "trade_date": "20240902", "turnover_rate": 2.2, "volume_ratio": 1.1, "total_mv": 2000.0, "circ_mv": 1500.0, "pe_ttm": 6.2, "pb": 0.7, "ps_ttm": 1.2, "dv_ttm": None},
|
|
],
|
|
"adj_factor": [
|
|
{"ts_code": "600000.SH", "trade_date": "20240902", "adj_factor": 1.1},
|
|
{"ts_code": "000001.SZ", "trade_date": "20240902", "adj_factor": 2.0},
|
|
],
|
|
"index_daily": [
|
|
{"ts_code": "000001.SH", "trade_date": "20240902", "open": 2700, "high": 2750, "low": 2690, "close": 2740, "pct_chg": 0.5, "vol": 3000.0, "amount": 500000.0},
|
|
{"ts_code": "399001.SZ", "trade_date": "20240902", "open": 8000, "high": 8100, "low": 7900, "close": 8050, "pct_chg": 0.4, "vol": 2000.0, "amount": 300000.0},
|
|
{"ts_code": "399006.SZ", "trade_date": "20240902", "open": 1600, "high": 1620, "low": 1580, "close": 1610, "pct_chg": 0.3, "vol": 1000.0, "amount": 100000.0},
|
|
{"ts_code": "000300.SH", "trade_date": "20240902", "open": 3500, "high": 3550, "low": 3480, "close": 3520, "pct_chg": 0.2, "vol": 1500.0, "amount": 200000.0},
|
|
],
|
|
"moneyflow": [
|
|
{"ts_code": "600000.SH", "trade_date": "20240902", "buy_sm_amount": 10, "sell_sm_amount": 8, "buy_md_amount": 20, "sell_md_amount": 15, "buy_lg_amount": 30, "sell_lg_amount": 25, "buy_elg_amount": 40, "sell_elg_amount": 35, "net_mf_amount": 17},
|
|
{"ts_code": "000001.SZ", "trade_date": "20240902", "buy_sm_amount": 11, "sell_sm_amount": 9, "buy_md_amount": 21, "sell_md_amount": 16, "buy_lg_amount": 31, "sell_lg_amount": 26, "buy_elg_amount": 41, "sell_elg_amount": 36, "net_mf_amount": 18},
|
|
],
|
|
"stk_auction": [
|
|
{"ts_code": "600000.SH", "trade_date": "20240902", "vol": 100, "price": 10.15, "amount": 1500000, "pre_close": 10.00, "turnover_rate": 0.1, "volume_ratio": 1.2, "float_share": 2000},
|
|
{"ts_code": "000001.SZ", "trade_date": "20240902", "vol": 80, "price": 11.05, "amount": 1200000, "pre_close": 11.10, "turnover_rate": 0.2, "volume_ratio": 0.9, "float_share": 1800},
|
|
],
|
|
"limit_list_d": [
|
|
{"trade_date": "20240902", "ts_code": "600000.SH", "industry": "银行", "name": "浦发银行", "close": 10.2, "pct_chg": 9.95, "amount": 1e8, "limit_amount": 5000, "float_mv": 800, "total_mv": 1000, "turnover_ratio": 5.0, "fd_amount": 2e7, "first_time": "09:30:01", "last_time": "14:55:00", "open_times": 0, "up_stat": "1/1", "limit_times": 1, "limit_type": "U"},
|
|
],
|
|
"ths_hot": [
|
|
{"ts_code": "600000.SH", "ts_name": "浦发银行", "hot": 90.0, "rank": 1, "pct_change": 1.2, "current_price": 10.2, "concept": "银行", "data_type": "热股", "trade_date": "20240902"},
|
|
],
|
|
"dc_hot": [
|
|
{"ts_code": "600000.SH", "ts_name": "浦发银行", "rank": 2, "pct_change": 1.2, "current_price": 10.2, "hot": 80.0, "concept": "银行", "data_type": "A股市场", "trade_date": "20240902"},
|
|
],
|
|
"hm_detail": [
|
|
{"trade_date": "20240902", "ts_code": "600000.SH", "ts_name": "浦发银行", "buy_amount": 1000, "sell_amount": 200, "net_amount": 800, "hm_name": "测试游资", "hm_orgs": "某某营业部", "tag": "超买"},
|
|
],
|
|
"top_list": [
|
|
{"trade_date": "20240902", "ts_code": "600000.SH", "name": "浦发银行", "pct_change": 9.95, "reason": "涨幅偏离值达7%"},
|
|
],
|
|
"ths_daily": [
|
|
{"ts_code": "885811.TI", "trade_date": "20240902", "open": 1000, "high": 1010, "low": 990, "close": 1005, "pre_close": 995, "pct_change": 1.0, "vol": 100, "turnover_rate": 1.2},
|
|
],
|
|
"dc_index": [
|
|
{"ts_code": "BK0475", "trade_date": "20240902", "name": "银行", "open": 100, "high": 101, "low": 99, "close": 100.5, "pre_close": 99.5, "pct_change": 1.0, "vol": 10, "amount": 1e8, "turnover_rate": 0.5},
|
|
],
|
|
"sw_daily": [
|
|
{"ts_code": "801780.SI", "trade_date": "20240902", "name": "银行", "open": 2000, "high": 2010, "low": 1990, "close": 2005, "pct_change": 0.8, "vol": 50, "amount": 2e8},
|
|
],
|
|
"stk_limit": [
|
|
{"ts_code": "600000.SH", "trade_date": "20240902", "up_limit": 11.22, "down_limit": 9.18},
|
|
{"ts_code": "000001.SZ", "trade_date": "20240902", "up_limit": 12.21, "down_limit": 9.99},
|
|
],
|
|
"index_member_all": [
|
|
{
|
|
"l2_code": "801780.SI",
|
|
"l2_name": "银行",
|
|
"ts_code": "600000.SH",
|
|
"name": "浦发银行",
|
|
"in_date": "20140101",
|
|
"out_date": "",
|
|
"is_new": "Y",
|
|
}
|
|
],
|
|
"hm_list": [{"name": "测试游资", "desc": "测试", "orgs": "某某营业部"}],
|
|
}
|
|
|
|
|
|
def fake_transport(api_name: str, params: dict, fields: str):
|
|
if api_name == "index_daily":
|
|
code = params.get("ts_code")
|
|
rows = [copy.deepcopy(row) for row in RAW["index_daily"] if row["ts_code"] == code]
|
|
trade_date = str(params.get("trade_date") or "")
|
|
start = str(params.get("start_date") or "")
|
|
end = str(params.get("end_date") or "")
|
|
if trade_date:
|
|
rows = [row for row in rows if row["trade_date"] == trade_date]
|
|
if start:
|
|
rows = [row for row in rows if row["trade_date"] >= start]
|
|
if end:
|
|
rows = [row for row in rows if row["trade_date"] <= end]
|
|
return rows
|
|
if api_name == "trade_cal":
|
|
start = str(params.get("start_date") or "")
|
|
end = str(params.get("end_date") or "99999999")
|
|
return [copy.deepcopy(row) for row in RAW["trade_cal"] if start <= row["cal_date"] <= end]
|
|
rows = copy.deepcopy(list(RAW.get(api_name) or []))
|
|
if api_name == "limit_list_d":
|
|
limit_type = str(params.get("limit_type") or "")
|
|
if limit_type:
|
|
rows = [row for row in rows if str(row.get("limit_type") or "") == limit_type]
|
|
return rows
|
|
|
|
|
|
class StubSiteAuth:
|
|
"""Stand-in for the review-site session bridge.
|
|
|
|
The console verifies operators against the review site over HTTP, which
|
|
tests must not depend on. This stub answers from a fixed session -> user
|
|
map and keeps the same stateless-HMAC CSRF contract as the real service, so
|
|
tests exercise the console's own gate rather than the network hop.
|
|
"""
|
|
|
|
SESSION = "site-session-token"
|
|
ADMIN = {"id": 1, "username": "admin", "role": "admin", "is_admin": True}
|
|
MEMBER = {"id": 2, "username": "member", "role": "user", "is_admin": False}
|
|
|
|
def __init__(self, password: str = "AdminPass1", secret: str = "stub-secret") -> None:
|
|
self.password = password
|
|
self.secret = secret
|
|
self.sessions = {self.SESSION: dict(self.ADMIN)}
|
|
self.logged_out: list[str] = []
|
|
|
|
def add_session(self, token: str, user: dict) -> None:
|
|
self.sessions[token] = dict(user)
|
|
|
|
def verify(self, session_token: str):
|
|
return self.sessions.get(session_token)
|
|
|
|
def csrf_token(self, session_token: str) -> str:
|
|
import hashlib
|
|
import hmac
|
|
|
|
digest = hashlib.sha256(session_token.encode("utf-8")).digest()
|
|
return hmac.new(self.secret.encode("utf-8"), digest, hashlib.sha256).hexdigest()
|
|
|
|
def check_csrf(self, session_token: str, supplied: str) -> bool:
|
|
import hmac
|
|
|
|
return bool(supplied) and hmac.compare_digest(self.csrf_token(session_token), supplied)
|
|
|
|
def logout(self, session_token: str) -> None:
|
|
self.logged_out.append(session_token)
|
|
self.sessions.pop(session_token, None)
|
|
|
|
def confirm_password(self, user_id: int, password: str) -> bool:
|
|
return bool(password) and password == self.password
|
|
|
|
def invalidate(self, session_token: str) -> None:
|
|
pass
|