feat: 重构定投评估工具并优化Cloud工具体验

- 重构市场定投评估逻辑:优先读取本地估值快照,必要时通过探针补抓海外数据,后端接口新增force参数支持强制刷新,更新前后端注释与交互文案
- 优化Cloud工具全链路:
  - 新增运营商登录页地址字段,支持一键复制账号密码并打开对应登录页
  - 缓存密钥派生结果,避免重复阻塞事件循环,提升列表加载性能
  - 完善接口错误处理,新增面板级加载失败提示与自动重试功能
  - 修复云资源代理转发时的Cookie缺失问题
  - 并行加载所有面板数据,优化首屏渲染速度
  - 新增登录按钮样式与错误提示样式
- 新增4个Cloud工具调试脚本,用于性能测试、解密审计、回归验证与加载耗时统计
- 启动定投评估工具的定时调度任务
This commit is contained in:
yangxiangyuan
2026-08-17 16:33:41 +08:00
parent 4016636153
commit e4f84caa23
10 changed files with 1149 additions and 237 deletions
+16 -4
View File
@@ -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
View File
@@ -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) => {
File diff suppressed because it is too large Load Diff