feat: 重构定投评估工具并优化Cloud工具体验
- 重构市场定投评估逻辑:优先读取本地估值快照,必要时通过探针补抓海外数据,后端接口新增force参数支持强制刷新,更新前后端注释与交互文案 - 优化Cloud工具全链路: - 新增运营商登录页地址字段,支持一键复制账号密码并打开对应登录页 - 缓存密钥派生结果,避免重复阻塞事件循环,提升列表加载性能 - 完善接口错误处理,新增面板级加载失败提示与自动重试功能 - 修复云资源代理转发时的Cookie缺失问题 - 并行加载所有面板数据,优化首屏渲染速度 - 新增登录按钮样式与错误提示样式 - 新增4个Cloud工具调试脚本,用于性能测试、解密审计、回归验证与加载耗时统计 - 启动定投评估工具的定时调度任务
This commit is contained in:
+16
-4
@@ -8,11 +8,21 @@ const cfgPath = path.join(process.cwd(), 'config', 'cloud.json')
|
||||
if (!fs.existsSync(path.dirname(cfgPath))) fs.mkdirSync(path.dirname(cfgPath), { recursive: true })
|
||||
if (!fs.existsSync(cfgPath)) fs.writeFileSync(cfgPath, JSON.stringify({ machine_salt: crypto.randomBytes(16).toString('hex') }, null, 2))
|
||||
const getConfig = () => JSON.parse(fs.readFileSync(cfgPath, 'utf-8'))
|
||||
// 派生密钥进程级缓存:scryptSync 单次约 70ms+ 且同步阻塞事件循环,
|
||||
// 逐行解密时重复派生会导致列表接口秒级卡顿(operators/ecs/vas 各 1s+)。
|
||||
// 仅当 passphrase 或 machine_salt 变化时才重新派生。
|
||||
let derivedKeyCache = null
|
||||
let derivedKeyCacheSig = ''
|
||||
const deriveKey = () => {
|
||||
const cfg = getConfig()
|
||||
const salt = Buffer.from(String(cfg.machine_salt || 'default'), 'utf-8')
|
||||
const saltStr = String(cfg.machine_salt || '')
|
||||
const passphrase = process.env.CLOUD_PASSPHRASE || process.env.SYSTEM_AUTH_PASS_93220 || ''
|
||||
return crypto.scryptSync(passphrase + String(cfg.machine_salt || ''), salt, 32)
|
||||
const sig = passphrase + '|' + saltStr
|
||||
if (derivedKeyCache && sig === derivedKeyCacheSig) return derivedKeyCache
|
||||
const salt = Buffer.from(saltStr || 'default', 'utf-8')
|
||||
derivedKeyCache = crypto.scryptSync(passphrase + saltStr, salt, 32)
|
||||
derivedKeyCacheSig = sig
|
||||
return derivedKeyCache
|
||||
}
|
||||
const enc = plain => {
|
||||
const key = deriveKey()
|
||||
@@ -39,6 +49,7 @@ db.exec(`CREATE TABLE IF NOT EXISTS cloud_operators (
|
||||
login_password TEXT,
|
||||
mobile TEXT,
|
||||
contact_person TEXT,
|
||||
login_url TEXT,
|
||||
remark TEXT,
|
||||
is_company_verified INTEGER DEFAULT 0,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
@@ -109,6 +120,7 @@ const ensureCols = () => {
|
||||
if (!hasCol('cloud_operators','login_password_enc')) db.exec(`ALTER TABLE cloud_operators ADD COLUMN login_password_enc BLOB`)
|
||||
if (!hasCol('cloud_operators','login_password_iv')) db.exec(`ALTER TABLE cloud_operators ADD COLUMN login_password_iv BLOB`)
|
||||
if (!hasCol('cloud_operators','login_password_tag')) db.exec(`ALTER TABLE cloud_operators ADD COLUMN login_password_tag BLOB`)
|
||||
if (!hasCol('cloud_operators','login_url')) db.exec(`ALTER TABLE cloud_operators ADD COLUMN login_url TEXT`)
|
||||
if (!hasCol('cloud_operators','remark')) db.exec(`ALTER TABLE cloud_operators ADD COLUMN remark TEXT`)
|
||||
if (!hasCol('cloud_ecs','rdp_password_enc')) db.exec(`ALTER TABLE cloud_ecs ADD COLUMN rdp_password_enc BLOB`)
|
||||
if (!hasCol('cloud_ecs','rdp_password_iv')) db.exec(`ALTER TABLE cloud_ecs ADD COLUMN rdp_password_iv BLOB`)
|
||||
@@ -148,8 +160,8 @@ const migrateExisting = () => {
|
||||
migrateExisting()
|
||||
|
||||
const listOperators = db.prepare(`SELECT * FROM cloud_operators ORDER BY id DESC`)
|
||||
const createOperatorStmt = db.prepare(`INSERT INTO cloud_operators (name, login_account, login_password, mobile, contact_person, remark, is_company_verified, login_password_enc, login_password_iv, login_password_tag) VALUES (@name, @login_account, '', @mobile, @contact_person, @remark, @is_company_verified, @login_password_enc, @login_password_iv, @login_password_tag)`)
|
||||
const updateOperatorStmt = db.prepare(`UPDATE cloud_operators SET name=@name, login_account=@login_account, login_password='', mobile=@mobile, contact_person=@contact_person, remark=@remark, is_company_verified=@is_company_verified, login_password_enc=@login_password_enc, login_password_iv=@login_password_iv, login_password_tag=@login_password_tag, updated_at=datetime('now') WHERE id=@id`)
|
||||
const createOperatorStmt = db.prepare(`INSERT INTO cloud_operators (name, login_account, login_password, mobile, contact_person, login_url, remark, is_company_verified, login_password_enc, login_password_iv, login_password_tag) VALUES (@name, @login_account, '', @mobile, @contact_person, @login_url, @remark, @is_company_verified, @login_password_enc, @login_password_iv, @login_password_tag)`)
|
||||
const updateOperatorStmt = db.prepare(`UPDATE cloud_operators SET name=@name, login_account=@login_account, login_password='', mobile=@mobile, contact_person=@contact_person, login_url=@login_url, remark=@remark, is_company_verified=@is_company_verified, login_password_enc=@login_password_enc, login_password_iv=@login_password_iv, login_password_tag=@login_password_tag, updated_at=datetime('now') WHERE id=@id`)
|
||||
const deleteOperatorStmt = db.prepare(`DELETE FROM cloud_operators WHERE id=?`)
|
||||
|
||||
const listEcs = db.prepare(`SELECT * FROM cloud_ecs ORDER BY id DESC`)
|
||||
|
||||
+7
-4
@@ -4784,10 +4784,10 @@ app.get('/api/markets/weekly_report', async (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
// 建行定投评估:按需抓取四大指数 PE-TTM 与近10年分位(点击页面按钮触发,无定时任务)
|
||||
// 建行定投评估:A股按需评估;海外部分优先读取本地快照,必要时通过美西探针补抓并持久化
|
||||
app.get('/api/markets/dip_eval', async (req, res) => {
|
||||
try {
|
||||
const payload = await marketsDipEval.getDipEvalReport()
|
||||
const payload = await marketsDipEval.getDipEvalReport({ force: String(req.query.force || '') === '1' })
|
||||
// 统一 200 + ok 标志:整体失败时 ok=false,前端交互弹窗读取 error 文案
|
||||
res.json(payload || { ok: false, error: '评估失败' })
|
||||
} catch (e) {
|
||||
@@ -5626,7 +5626,7 @@ app.get('/api/cloud/balance_alerts', (req, res) => {
|
||||
app.get('/api/cloud/balance_alerts/status', (req, res) => {
|
||||
;(async () => {
|
||||
try {
|
||||
const forwarded = await proxyCloudBalanceAutomationStatus('/api/cloud/balance_alerts/status')
|
||||
const forwarded = await proxyCloudBalanceAutomationStatus('/api/cloud/balance_alerts/status', req)
|
||||
if (forwarded) return res.status(forwarded.status).json(forwarded.body)
|
||||
res.json({ ok: true, data: cloudBalanceWatch.getStatus() })
|
||||
} catch (e) { res.status(500).json({ ok: false, error: String(e.message || e) }) }
|
||||
@@ -5665,13 +5665,15 @@ const proxyCloudBalanceAutomationRequest = async (req, routePath) => {
|
||||
})
|
||||
}
|
||||
}
|
||||
const proxyCloudBalanceAutomationStatus = async routePath => {
|
||||
const proxyCloudBalanceAutomationStatus = async (routePath, req) => {
|
||||
const cfg = cloudBalanceWatch.readConfig()
|
||||
const currentPort = Number(process.env.PORT || '8976') || 8976
|
||||
const automationPort = Number(cfg.automation_port || 8977) || 8977
|
||||
if (currentPort === automationPort) return null
|
||||
const url = `http://127.0.0.1:${automationPort}${routePath}`
|
||||
// 必须转发 Cookie,否则 8977 的 /api/cloud 守卫会返回 401
|
||||
const proxied = await axios.get(url, {
|
||||
headers: { Cookie: (req && req.headers && req.headers.cookie) || '' },
|
||||
timeout: 15000,
|
||||
validateStatus: () => true
|
||||
})
|
||||
@@ -7376,6 +7378,7 @@ if (currentPort === marketsAutomationPort) {
|
||||
securityCalendar.startScheduler()
|
||||
securityCalendarBridge.startScheduler(securityCalendar)
|
||||
marketsWeeklyReport.startScheduler()
|
||||
marketsDipEval.startScheduler()
|
||||
globalNews.startScheduler()
|
||||
|
||||
app.use('/api/yuanzhupai', (req, res, next) => {
|
||||
|
||||
+781
-209
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user