feat: 新增免费演出雷达工具与定投评估功能,优化周报提醒逻辑
1. 新增免费演出雷达工具:含秀动/猫眼/大麦平台抓取、本地代理鉴权配置 2. 新增建行定投评估模块:四大指数PE-TTM估值与定投档位建议 3. 新增推送告警白名单过滤功能,限制推送指定指数告警 4. 重构周报首页提醒逻辑,移至服务端处理并减少前端请求 5. 新增多套调试探针脚本与单元测试用例
This commit is contained in:
+150
-2
@@ -51,10 +51,12 @@ const marketSkills = require('./market_skills/manager')
|
||||
const securityCalendar = require('./security_calendar')
|
||||
const securityCalendarBridge = require('./security_calendar_calendar_bridge')
|
||||
const marketsWeeklyReport = require('./markets_weekly_report')
|
||||
const marketsDipEval = require('./markets_dip_eval')
|
||||
const { upsertMarketArchive, getMarketArchive } = require('./db')
|
||||
const { getConfig: getWallCfg, setConfig: setWallCfg, listCards: listWallCards, createCard: createWallCard, updateCardById: updateWallCardById, deleteCardById: deleteWallCardById } = require('./wall')
|
||||
const fundsGuoxin = require('./funds_guoxin')
|
||||
const fundsHuatai = require('./funds_huatai')
|
||||
const freeShow = require('./free_show')
|
||||
const ccbPrivateFunds = require('./ccb_private_funds')
|
||||
const cloud = require('./cloud')
|
||||
const cloudBalanceWatch = require('./cloud_balance_watch')
|
||||
@@ -223,6 +225,7 @@ app.use((req, res, next) => {
|
||||
req.path.startsWith('/api/doc_calendar_bridge') ||
|
||||
req.path.startsWith('/api/markets/security_calendar') ||
|
||||
req.path.startsWith('/api/markets/weekly_report') ||
|
||||
req.path.startsWith('/api/markets/dip_eval') ||
|
||||
req.path.startsWith('/api/global_news') ||
|
||||
req.path.startsWith('/api/box_backup')
|
||||
) setNoCache(res)
|
||||
@@ -1212,7 +1215,7 @@ if (!fs.existsSync(settingsPath)) fs.writeFileSync(settingsPath, JSON.stringify(
|
||||
const flagsPath = path.join(process.cwd(), 'config', 'flags.json')
|
||||
if (!fs.existsSync(flagsPath)) fs.writeFileSync(flagsPath, JSON.stringify({
|
||||
debugTool: { enabled: true, launchDate: null, autoHideAfterDays: 30 },
|
||||
debug: { weibo: true, markets: true, wall: true, funds_guoxin: true, funds_huatai: true },
|
||||
debug: { weibo: true, markets: true, wall: true, funds_guoxin: true, funds_huatai: true, free_show: true },
|
||||
navAuth: { enable_auth: true, iss: 'TRAE-NAV', iss_strict: false }
|
||||
}, null, 2))
|
||||
|
||||
@@ -1611,6 +1614,7 @@ const mapSystemIdToUrl = (systemId) => {
|
||||
'Tools-wall-': '/tools/wall',
|
||||
'Tools-funds_guoxin': '/tools/funds_guoxin',
|
||||
'Tools-funds_huatai': '/tools/funds_huatai',
|
||||
'Tools-free_show': '/tools/free_show',
|
||||
'Tools-ccb_private_funds': '/tools/ccb_private_funds',
|
||||
'Tools-cloud': '/tools/cloud',
|
||||
'Tools-weekly': '/tools/weekly',
|
||||
@@ -2047,6 +2051,136 @@ app.use('/api/funds_huatai', (req, res, next) => {
|
||||
return res.status(401).json({ ok: false, error: 'unauthorized' })
|
||||
})
|
||||
|
||||
// ============ free_show(免费演出雷达)鉴权 ============
|
||||
const FREE_SHOW_AUTH_COOKIE = 'free_show_gate'
|
||||
const readFreeShowAuthConfig = () => {
|
||||
try {
|
||||
const p = path.join(process.cwd(), 'public', 'tools', 'free_show', 'auth_config.json')
|
||||
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
|
||||
} catch {}
|
||||
return {}
|
||||
}
|
||||
const readFreeShowJwk = () => {
|
||||
try {
|
||||
const p = path.join(process.cwd(), 'config', 'free_show.jwk.json')
|
||||
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
|
||||
} catch {}
|
||||
return { kty: 'RSA', n: '', e: '' }
|
||||
}
|
||||
const getFreeShowCookieName = () => {
|
||||
try { const n = String(readFreeShowAuthConfig().cookieName || ''); if (n) return n } catch {}
|
||||
return FREE_SHOW_AUTH_COOKIE
|
||||
}
|
||||
const getFreeShowMaxAge = () => {
|
||||
try {
|
||||
const days = Number(readFreeShowAuthConfig().max_age_days || 0)
|
||||
if (Number.isFinite(days) && days > 0) return Math.min(days, 365) * 24 * 3600 * 1000
|
||||
} catch {}
|
||||
return 30 * 24 * 3600 * 1000
|
||||
}
|
||||
const getFreeShowGuidConfig = () => {
|
||||
try {
|
||||
const cfg = readFreeShowAuthConfig()
|
||||
const keyRaw = String(cfg.guidParam || cfg.guid_param || 'guid').trim()
|
||||
const valueRaw = String(cfg.guidValue || cfg.guid_value || '').trim()
|
||||
return { key: keyRaw || 'guid', value: valueRaw }
|
||||
} catch { return { key: 'guid', value: '' } }
|
||||
}
|
||||
const hasValidFreeShowGuid = req => {
|
||||
try {
|
||||
const cfg = readFreeShowAuthConfig()
|
||||
if (cfg.enable_auth === false) return false
|
||||
const { key, value } = getFreeShowGuidConfig()
|
||||
if (!value) return false
|
||||
const q = req && req.query ? req.query : {}
|
||||
const raw = q[key]
|
||||
if (raw === undefined || raw === null) return false
|
||||
const incoming = Array.isArray(raw) ? String(raw[0] || '') : String(raw)
|
||||
return incoming === value
|
||||
} catch { return false }
|
||||
}
|
||||
const isFreeShowForceGuid = () => {
|
||||
try {
|
||||
const cfg = readFreeShowAuthConfig()
|
||||
if (cfg.enable_auth === false) return false
|
||||
if (Object.prototype.hasOwnProperty.call(cfg, 'requireGuid')) return !!cfg.requireGuid
|
||||
if (Object.prototype.hasOwnProperty.call(cfg, 'require_guid')) return !!cfg.require_guid
|
||||
return false
|
||||
} catch { return false }
|
||||
}
|
||||
const hasFreeShowAuth = req => {
|
||||
try {
|
||||
const config = readFreeShowAuthConfig()
|
||||
if (config.enable_auth === false) return true
|
||||
const cookies = parseCookie(req.headers.cookie || '')
|
||||
return cookies[getFreeShowCookieName()] === '1'
|
||||
} catch { return false }
|
||||
}
|
||||
app.get('/tools/free_show', (req, res, next) => {
|
||||
setNoCache(res)
|
||||
try {
|
||||
const forceGuid = isFreeShowForceGuid()
|
||||
if (hasFreeShowAuth(req)) return next()
|
||||
const guidKey = getFreeShowGuidConfig().key
|
||||
const hasGuidInput = req && req.query && Object.prototype.hasOwnProperty.call(req.query, guidKey)
|
||||
if (hasValidFreeShowGuid(req)) {
|
||||
const maxAge = getFreeShowMaxAge()
|
||||
const cookieName = getFreeShowCookieName()
|
||||
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' })
|
||||
return res.redirect('/tools/free_show/index.html')
|
||||
}
|
||||
if (hasGuidInput) return res.status(401).send('未授权')
|
||||
const token = String(req.query.token || '')
|
||||
if (!token) {
|
||||
if (forceGuid) return res.status(401).send('未授权')
|
||||
return next()
|
||||
}
|
||||
const payload = verifyJwtWithKeys(token, jwkKeys(readFreeShowJwk()))
|
||||
if (!payload) return res.status(401).send('未授权')
|
||||
{
|
||||
const iss = String(payload.iss || '')
|
||||
const navIss = getNavIssFromFlags()
|
||||
if (iss === navIss && !audMatch(payload, 'Tools-free_show')) return res.status(401).send('未授权')
|
||||
}
|
||||
const maxAge = getFreeShowMaxAge()
|
||||
const cookieName = getFreeShowCookieName()
|
||||
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' })
|
||||
return res.redirect('/tools/free_show/index.html')
|
||||
} catch (e) { return res.status(401).send('未授权') }
|
||||
})
|
||||
app.use('/tools/free_show', (req, res, next) => {
|
||||
const forceGuid = isFreeShowForceGuid()
|
||||
if (hasFreeShowAuth(req)) return next()
|
||||
const isHandshake = req.method === 'GET' && (req.path === '/' || req.path === '' || req.path === '/index.html')
|
||||
const guidKey = getFreeShowGuidConfig().key
|
||||
const hasGuidInput = isHandshake && req && req.query && Object.prototype.hasOwnProperty.call(req.query, guidKey)
|
||||
if (isHandshake && hasValidFreeShowGuid(req)) {
|
||||
const maxAge = getFreeShowMaxAge()
|
||||
const cookieName = getFreeShowCookieName()
|
||||
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' })
|
||||
return res.redirect('/tools/free_show/index.html')
|
||||
}
|
||||
if (hasGuidInput) return res.status(401).send('未授权')
|
||||
if (isHandshake && String(req.query.token || '')) {
|
||||
const payload = verifyJwtWithKeys(String(req.query.token), jwkKeys(readFreeShowJwk()))
|
||||
if (payload) {
|
||||
const iss = String(payload.iss || '')
|
||||
const navIss = getNavIssFromFlags()
|
||||
if (iss === navIss && !audMatch(payload, 'Tools-free_show')) return res.status(401).send('未授权')
|
||||
const maxAge = getFreeShowMaxAge()
|
||||
const cookieName = getFreeShowCookieName()
|
||||
res.cookie(cookieName, '1', { httpOnly: true, sameSite: 'lax', maxAge, path: '/' })
|
||||
return res.redirect('/tools/free_show/index.html')
|
||||
}
|
||||
}
|
||||
if (isHandshake && forceGuid) return res.status(401).send('未授权')
|
||||
return res.status(401).send('未授权')
|
||||
})
|
||||
app.use('/api/free_show', (req, res, next) => {
|
||||
if (hasFreeShowAuth(req)) return next()
|
||||
return res.status(401).json({ ok: false, error: 'unauthorized' })
|
||||
})
|
||||
|
||||
const CCB_PRIVATE_FUNDS_AUTH_COOKIE = 'ccb_private_funds_gate'
|
||||
const readCcbPrivateFundsJwk = () => {
|
||||
try {
|
||||
@@ -4104,6 +4238,7 @@ app.get('/api/debug/jwt/verify', (req, res) => {
|
||||
'cloud': () => readCloudJwk(),
|
||||
'funds_guoxin': () => readFundsGuoxinJwk(),
|
||||
'funds_huatai': () => readFundsHuataiJwk(),
|
||||
'free_show': () => readFreeShowJwk(),
|
||||
'markets': () => readMarketsJwk(),
|
||||
'wall': () => readWallJwk(),
|
||||
'weekly': () => readWeeklyJwk(),
|
||||
@@ -4257,7 +4392,7 @@ app.post('/api/token_lab/nav/verify', (req, res) => {
|
||||
|
||||
app.get('/api/flags', (req, res) => {
|
||||
const enabled = computeDebugEnabled()
|
||||
res.json({ ok: true, debugToolEnabled: enabled, debug: { weibo: computeToolDebug('weibo'), markets: computeToolDebug('markets'), wall: computeToolDebug('wall'), funds_guoxin: computeToolDebug('funds_guoxin'), funds_huatai: computeToolDebug('funds_huatai'), weekly: computeToolDebug('weekly') } })
|
||||
res.json({ ok: true, debugToolEnabled: enabled, debug: { weibo: computeToolDebug('weibo'), markets: computeToolDebug('markets'), wall: computeToolDebug('wall'), funds_guoxin: computeToolDebug('funds_guoxin'), funds_huatai: computeToolDebug('funds_huatai'), free_show: computeToolDebug('free_show'), weekly: computeToolDebug('weekly') } })
|
||||
})
|
||||
|
||||
app.get('/api/weibo/devtools/status', async (req, res) => {
|
||||
@@ -4625,6 +4760,17 @@ app.get('/api/markets/weekly_report', async (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
// 建行定投评估:按需抓取四大指数 PE-TTM 与近10年分位(点击页面按钮触发,无定时任务)
|
||||
app.get('/api/markets/dip_eval', async (req, res) => {
|
||||
try {
|
||||
const payload = await marketsDipEval.getDipEvalReport()
|
||||
// 统一 200 + ok 标志:整体失败时 ok=false,前端交互弹窗读取 error 文案
|
||||
res.json(payload || { ok: false, error: '评估失败' })
|
||||
} catch (e) {
|
||||
res.status(500).json({ ok: false, error: String(e && e.message ? e.message : e) })
|
||||
}
|
||||
})
|
||||
|
||||
app.post('/api/markets/config', (req, res) => {
|
||||
const body = req.body || {}
|
||||
const cfg = getMarketsCfg()
|
||||
@@ -6838,6 +6984,7 @@ app.get('/api/short_link/auth/hint', (req, res) => {
|
||||
|
||||
// Short Link 业务路由(/s/:code 公开跳转 + /api/short_link/* 管理接口)
|
||||
shortLink.bindRoutes(app)
|
||||
freeShow.bindRoutes(app)
|
||||
|
||||
const marketsCfgAtBoot = getMarketsCfg()
|
||||
const currentPort = Number(process.env.PORT || '8976') || 8976
|
||||
@@ -6845,6 +6992,7 @@ const marketsAutomationPort = Number(marketsCfgAtBoot.automation_port || 8977) |
|
||||
if (currentPort === marketsAutomationPort) {
|
||||
startMarketsScheduler()
|
||||
marketSkills.start()
|
||||
freeShow.startScheduler()
|
||||
}
|
||||
securityCalendar.startScheduler()
|
||||
securityCalendarBridge.startScheduler(securityCalendar)
|
||||
|
||||
Reference in New Issue
Block a user