feat: 新增免费演出雷达工具与定投评估功能,优化周报提醒逻辑

1. 新增免费演出雷达工具:含秀动/猫眼/大麦平台抓取、本地代理鉴权配置
2. 新增建行定投评估模块:四大指数PE-TTM估值与定投档位建议
3. 新增推送告警白名单过滤功能,限制推送指定指数告警
4. 重构周报首页提醒逻辑,移至服务端处理并减少前端请求
5. 新增多套调试探针脚本与单元测试用例
This commit is contained in:
yangxiangyuan
2026-08-04 17:33:14 +08:00
parent af5d0f43a5
commit 6a7a2b089a
25 changed files with 3499 additions and 121 deletions
@@ -0,0 +1,32 @@
/**
* 单元测试:markets_dip_eval 建行定投评估模块
* 用法:node dev_test_scripts/unit/test_markets_dip_eval.js
*/
process.chdir(require('path').join(__dirname, '..', '..'))
const { getDipEvalReport } = require('../../src/server/markets_dip_eval')
;(async () => {
try {
const payload = await getDipEvalReport()
console.log('=== 返回 JSON ===')
console.log(JSON.stringify(payload, null, 2))
if (!payload || payload.ok === false) {
console.log('\n[测试结果] 整体失败(可能是网络问题,见 errors)')
process.exit(1)
}
const expectKeys = ['name', 'code', 'market', 'pe_ttm', 'pe_date', 'percentile', 'source', 'advice', 'level']
let bad = 0
for (const row of payload.rows) {
for (const k of expectKeys) {
if (!(k in row)) { console.log(`[字段缺失] ${row.name} 缺少 ${k}`); bad++ }
}
if (!Number.isFinite(row.pe_ttm)) { console.log(`[PE无效] ${row.name} pe_ttm=${row.pe_ttm}`); bad++ }
}
console.log(bad === 0 ? '\n[测试结果] PASS:行数=' + payload.rows.length + ',字段完整,PE 有效' : `\n[测试结果] FAIL:${bad} 处问题`)
process.exit(bad === 0 ? 0 : 1)
} catch (e) {
console.log('[测试结果] 异常崩溃:', e)
process.exit(1)
}
})()
@@ -0,0 +1,69 @@
/**
* 单元测试:markets 推送告警白名单过滤 applyNotifyAllowlist
* 背景:日历/邮件推送只保留 8 个指定 symbol,其余卡片只展示不推送
* 运行:node dev_test_scripts/unit/test_markets_notify_allowlist.js
*/
const path = require('path')
// 切到仓库根目录,保证 markets.js 内 process.cwd() 相对路径正确
process.chdir(path.join(__dirname, '..', '..'))
const { applyNotifyAllowlist } = require(path.join(process.cwd(), 'src', 'server', 'markets.js'))
const ALLOWLIST = ['sh000001', 'sz399001', 'sh000300', 'sh000688', 'sz399006', 'usINX', 'usIXIC', 'hf_GC']
// 模拟全量 quotes:8 个保留项 + 若干应被排除的卡片
const mockQuotes = [
{ symbol: 'sh000001', label: '上证指数' },
{ symbol: 'sz399001', label: '深证成指' },
{ symbol: 'sh000300', label: '沪深300' },
{ symbol: 'sh000688', label: '科创50' },
{ symbol: 'sz399006', label: '创业板指' },
{ symbol: 'usINX', label: '标普500' },
{ symbol: 'usIXIC', label: '纳斯达克' },
{ symbol: 'hf_GC', label: 'COMEX黄金' },
// 以下应被排除
{ symbol: 'hf_SI', label: 'COMEX白银' },
{ symbol: 'usDJI', label: '道琼斯' },
{ symbol: 'hkHSI', label: '恒生指数' },
{ symbol: 'bj899050', label: '北证50' },
{ symbol: 'usEWG', label: '德国DAX30' }
]
let failed = 0
const assert = (cond, name) => {
if (cond) {
console.log(` PASS ${name}`)
} else {
failed += 1
console.log(` FAIL ${name}`)
}
}
console.log('== test: applyNotifyAllowlist ==')
// 1. 白名单生效:只保留 8 个
const r1 = applyNotifyAllowlist(mockQuotes, { notify_symbol_allowlist: ALLOWLIST })
assert(r1.kept.length === 8, '白名单模式保留 8 项')
assert(r1.excluded.length === 5, '白名单模式排除 5 项')
assert(r1.kept.every(q => ALLOWLIST.map(s => s.toLowerCase()).includes(String(q.symbol).toLowerCase())), '保留项全部在白名单内')
assert(r1.excluded.every(q => !ALLOWLIST.map(s => s.toLowerCase()).includes(String(q.symbol).toLowerCase())), '排除项全部不在白名单内')
// 2. 大小写不敏感
const r2 = applyNotifyAllowlist([{ symbol: 'USINX' }, { symbol: 'hf_gc' }], { notify_symbol_allowlist: ALLOWLIST })
assert(r2.kept.length === 2, 'symbol 匹配大小写不敏感')
// 3. 空白名单 = 不限制(兼容旧配置)
const r3 = applyNotifyAllowlist(mockQuotes, { notify_symbol_allowlist: [] })
assert(r3.kept.length === mockQuotes.length && r3.excluded.length === 0, '空白名单不过滤')
// 4. 缺省字段 = 不限制
const r4 = applyNotifyAllowlist(mockQuotes, {})
assert(r4.kept.length === mockQuotes.length, '缺省字段不过滤')
// 5. 异常输入不崩溃
const r5 = applyNotifyAllowlist(null, { notify_symbol_allowlist: ALLOWLIST })
assert(Array.isArray(r5.kept) && r5.kept.length === 0, 'quotes 为 null 时不崩溃')
console.log(failed === 0 ? '\nALL TESTS PASSED' : `\n${failed} TEST(S) FAILED`)
process.exit(failed === 0 ? 0 : 1)