我做了一个多端基金收益助手:从养基宝数据到 Web、桌面端、浏览器插件和 IDE 插件

项目地址:github.com/ChinaCarlos...

文档中心:chinacarlos.github.io/fund-helper...

Releases 下载:github.com/ChinaCarlos...

免责声明

  • 本项目仅用于个人学习、研究和技术交流。项目中对养基宝 browser-plug-api 的理解来自对公开浏览器插件网络交互的学习与反向工程分析;本项目非养基宝官方产品,与养基宝或任何第三方机构不存在从属、授权、合作或背书关系。请勿用于商业化、批量抓取、绕过限制或任何违反平台协议、法律法规的用途;因使用本项目或二次开发产生的任何责任与项目作者无关。
  • 项目展示的基金、股票、指数、收益等金融数据仅用于软件功能展示和技术研究,不构成投资建议、理财推荐或收益承诺。基金/股票有风险,投资需谨慎,请自行判断并承担风险。

如果你也有长期看基金、做定投、关注持仓收益的习惯,大概率遇到过一个很真实的问题:数据分散在不同平台里,想看今天整体赚了多少、哪个账户拖了后腿、哪些基金贡献最大,经常要来回打开 App、切账户、刷新、算收益。

我做的这个项目叫 Fund Helper 。它最初只是一个基于养基宝 browser-plug-api 的基金收益实时监控面板,后来逐步演进成了一个多端产品:Web 应用、Chrome 浏览器插件、VS Code/Cursor 扩展、JetBrains 插件,以及 Tauri 桌面端。它的目标不是替代交易软件,而是把"看持仓、看收益、看市场、收通知"这件事做得更集中、更轻、更适合日常使用。

这篇文章会从产品愿景、功能模块、系统架构、关键实现和工程化几个角度,复盘这个项目是怎么做出来的。

一、为什么要做 Fund Helper

基金投资有一个特点:决策频率不一定高,但关注频率往往很高。

很多人每天并不会频繁买卖基金,但会关注几个问题:

  • 今天我的总收益是多少?
  • 支付宝、天天基金、蛋卷等不同账户分别表现如何?
  • 哪些基金在涨,哪些基金在跌?
  • 市场风格现在偏向哪些行业或主题?
  • 能不能在收益变化时通过钉钉、飞书、企业微信收到提醒?
  • 能不能不打开手机,直接在电脑、浏览器或 IDE 里看一眼?

这就是 Fund Helper 的出发点:围绕"基金持仓状态"构建一个跨端、轻量、可部署、可扩展的信息面板

它有三个明确目标。

第一,降低查看成本。用户不需要频繁打开手机 App,也不需要在多个页面之间切换。打开 Web、浏览器插件、桌面端或者编辑器侧边栏,就能看到核心持仓状态。

第二,统一数据表达。养基宝接口返回的是比较原始的数据,不同字段在不同基金类型上还有差异。项目会把账户、基金、指数、收益曲线、涨跌方向等统一归一化,再交给各端展示。

第三,做成可长期演进的工程,而不是一次性脚本。项目中不仅有 Web 应用,还有独立插件、桌面端、IDE 扩展、Docker 部署、文档站和发版脚本,让它更接近一个完整产品。

二、它现在能做什么

Fund Helper 当前提供五种使用方式。

使用方式 适合场景 特点
Web 应用 完整功能面板 持仓、市场排行、板块热力图、通知、多用户管理
浏览器插件 快速查看 Chrome/Edge 工具栏 Popup,直连养基宝,无需部署后端
VS Code/Cursor 扩展 写代码时顺手看一眼 侧边栏、底部面板、状态栏多入口,适配编辑器主题
JetBrains 插件 IntelliJ/WebStorm/PyCharm 用户 Tool Window + 状态栏 + JCEF Webview
桌面端 本地单用户使用 Tauri + Rust + SQLite,支持托盘和本地通知配置

核心功能模块包括:

  • 微信扫码登录养基宝:通过二维码获取登录态,持仓相关接口基于 token 访问。
  • 持仓 Dashboard:展示总资产、当日收益、收益率、上涨/下跌基金数量。
  • 多账户分组:支持按支付宝、天天基金、蛋卷等账户分组查看。
  • 基金明细表格:展示基金代码、名称、持仓金额、成本、当日收益、涨跌幅等。
  • 收益曲线:拉取当日分时收益曲线,并支持汇总曲线与账户独立曲线。
  • 基金搜索与持仓管理:支持搜索基金、添加持仓、删除持仓。
  • 市场排行:基于 AKShare/东方财富数据展示全市场基金排行。
  • 板块热力图:查看行业/概念板块涨幅、资金流向,并下钻关联基金。
  • 通知推送:支持钉钉、飞书、企业微信,支持 Webhook 和部分企业应用投递模式。
  • 多用户和部署:Web 端支持管理员账号、多用户隔离、MongoDB 持久化、Docker 一体部署。

如果只是看一眼持仓,插件和 IDE 扩展足够轻;如果要看市场、配置通知、多用户使用,Web 应用更完整;如果希望本地独立运行,桌面端更合适。

三、整体架构:不是一个页面,而是一套多端运行模型

Fund Helper 的核心不是某一个页面,而是一套围绕"基金持仓快照"的运行模型。不同客户端的容器不同,但都会经过类似的链路:启动、检查登录态、扫码获取 token、拉取上游数据、归一化为快照、渲染 UI、处理刷新和失效。

整体架构可以这样看:

flowchart TD U[用户] --> W[Web 应用] U --> C[浏览器插件] U --> E[编辑器插件] U --> D[桌面端] W --> BFF[FastAPI BFF] BFF --> Mongo[(MongoDB)] BFF --> Notify[通知服务] BFF --> Market[AKShare / 东财市场数据] BFF --> YJB[养基宝 browser-plug-api] C --> ChromeStore[(chrome.storage.local)] C --> YJB E --> Host[Extension Host / Kotlin Host] Host --> IDEStore[(globalState / PersistentState)] Host --> YJB D --> Rust[Tauri Rust Commands] Rust --> SQLite[(SQLite)] Rust --> Tray[系统托盘 / 定时任务] Rust --> YJB

这里做了一个很关键的取舍:Web 端保留完整后端能力,插件、桌面端、编辑器插件则尽量独立。

Web 端需要多用户、通知配置、市场排行、热力图和 Docker 部署,所以它走 FastAPI BFF + MongoDB。浏览器插件和编辑器插件强调打开即用,如果还要求用户先部署后端,使用门槛会变高,所以它们直连养基宝。桌面端同样不依赖 Web 后端,但把 token、通知配置和推送节流信息放进本地 SQLite。

这就是这个项目的基本原则:核心业务模型统一,运行容器按端选择

四、Web 应用:BFF 承担"复杂性收口"

Web 应用是功能最完整的一端。它使用 React 19、TypeScript、Rsbuild、Ant Design 做前端,后端使用 FastAPI、httpx、Motor、MongoDB、AKShare。

它的实现流程不是"前端直接调一堆第三方接口",而是用 BFF 把第三方接口的不稳定性、签名、token、多用户隔离、通知配置全部收口。

Web 端完整形态如下:顶部是导航和刷新入口,中间是指数与汇总卡片,下面按账户 Tab 展示收益曲线和基金明细。页面本身只消费后端组装好的快照,不直接理解养基宝原始字段。

sequenceDiagram participant User as 用户 participant Web as React Web participant API as FastAPI BFF participant DB as MongoDB participant YJB as 养基宝 participant EM as 东财/AKShare participant Bot as 钉钉/飞书/企微 User->>Web: 打开 Dashboard Web->>API: 检查应用登录态 API->>DB: 读取 Session 和用户信息 API-->>Web: 已登录 / 需登录 / 需绑定养基宝 Web->>API: 请求持仓快照 API->>DB: 读取当前用户 yjb_token API->>YJB: 签名后拉取汇总、指数、账户基金 API->>API: 归一化 PortfolioSnapshot API-->>Web: 返回统一快照 Web->>Web: 渲染指数、收益卡片、账户 Tab、基金表格 Web->>API: 访问市场排行/热力图 API->>EM: 拉取市场公开数据 API-->>Web: 返回排行和板块数据 Web->>API: 保存通知配置或触发推送 API->>DB: 保存用户配置 API->>YJB: 推送前重新拉取最新快照 API->>Bot: 渲染并发送通知

BFF 层的价值在这里很明显。

第一,它隐藏了养基宝的签名规则。上游请求需要 AuthorizationRequest-TimeRequest-Sign,签名由路径、token、时间戳和密钥组合后 MD5 得到。前端不需要知道这些细节,只关心"我要一个持仓快照"。

第二,它把多个上游结果组合成一个页面可直接消费的模型。持仓页需要总资产、今日收益、各账户明细、基金列表、指数行情、交易时段标识。后端会拉取汇总、账户基金和指数,再统一组装成 PortfolioSnapshot

第三,它承载 Web 端的扩展能力。市场排行和板块热力图来自 AKShare/东方财富,通知配置存在 MongoDB,推送前复用同一套持仓快照能力。这些都不适合塞进前端。

浏览器插件的定位是"看一眼就够"。它使用 CRXJS、Vite、React 19、TypeScript、Manifest V3、chrome.storage.local,不依赖 FastAPI 和 MongoDB。

未登录时,Popup 会直接进入微信扫码页。这里的技术点是:插件端不经过后端创建登录会话,而是在 Popup 内生成二维码、轮询扫码状态,并在成功后把 token 写入 chrome.storage.local

它的运行流程如下:

graph TD A["点击浏览器工具栏图标"] --> B["加载 React Popup"] B --> C{"本地是否已有 token"} C --> D["未登录:LoginView 创建二维码"] D --> E["每 2 秒轮询扫码状态"] E --> F{"扫码是否成功"} F --> E F --> G["成功:保存 token 到 chrome.storage.local"] C --> H["已登录:fetchPortfolioSnapshot"] G --> H H --> I["直连养基宝并签名请求"] I --> J["拉取账户汇总、指数、基金列表"] J --> K["在插件侧组装 PortfolioSnapshot"] K --> L["PortfolioView 渲染指数、收益、账户 Tab、基金列表"] L --> M["定时静默刷新"] M --> H I --> N["401 失效:清空本地登录态并回到扫码页"]

这端有几个实现点值得单独说。

首先,插件的状态机很简单:boot -> login -> portfolio。启动时读取 chrome.storage.local,没有 token 就进入扫码登录,有 token 就直接拉持仓。请求返回 401 时清空本地 session,回到登录态。

其次,插件没有引入后端,所以签名、字段归一化、交易时段判断和持仓快照聚合都放在前端 TypeScript 里完成。这样用户加载插件就能用,但代价是关键算法需要和 Web 后端保持同步。

最后,Popup 空间非常有限。Chrome Popup 高度通常控制在 600px 左右,所以插件端没有做市场排行、热力图、复杂通知配置,而是聚焦在指数、收益卡片、多账户 Tab、基金排序这些最常用信息。

登录后,插件会把指数、总资产、当日收益、涨跌数量、账户 Tab、排序器和基金列表压缩进一个轻量面板里。这里没有服务端状态,刷新、排序、401 失效回退都在插件端完成。

六、桌面端:Tauri 把 React UI 和 Rust 本地能力接起来

桌面端的定位是"本地常驻 + 原生体验"。它使用 Tauri v2、Rust、React 19、Ant Design、Tailwind v4、SQLite。相比浏览器插件,它可以做更多本地能力:系统托盘、单实例、后台定时任务、本地通知配置、飞书/钉钉/企微推送。

桌面端除了主窗口,还做了 macOS 菜单栏/状态栏入口:菜单栏直接显示当日收益和收益率,点击后弹出轻量持仓面板。这个入口适合"工作中扫一眼",不需要切到完整应用窗口。

完整主窗口则承载更完整的 Dashboard:指数、资产卡片、账户分组、收益曲线和基金列表都在一个原生窗口里展示。前端仍然是 React,但所有登录态、持仓拉取和通知配置都通过 Tauri 命令交给 Rust 层处理。

桌面端不是 React 直接发 HTTP,而是通过 Tauri invoke 调用 Rust 命令。

sequenceDiagram participant UI as React UI participant Tauri as Tauri invoke participant Rust as Rust Commands participant DB as SQLite participant YJB as 养基宝 participant Tray as 系统托盘/菜单栏 participant Bot as 通知渠道 UI->>Tauri: get_auth_status Tauri->>Rust: 调用命令 Rust->>DB: 读取 app_profile Rust-->>UI: 已登录 / 未登录 UI->>Tauri: create_qr / poll_qr_state Rust->>YJB: 获取二维码并轮询状态 YJB-->>Rust: token、昵称、头像 Rust->>DB: 保存登录态 UI->>Tauri: fetch_portfolio Rust->>DB: 读取 token Rust->>YJB: 拉取汇总、指数、基金、曲线 Rust->>Rust: 归一化快照 Rust->>Tray: 同步菜单栏收益 Rust-->>UI: PortfolioSnapshot UI->>UI: 渲染持仓页 Rust->>Rust: 后台 scheduler 定时检查 Rust->>DB: 读取通知配置和上次推送时间 Rust->>YJB: 推送前拉取最新快照 Rust->>Bot: 发送收益通知

它的关键实现分层如下:

  • React UI:负责页面、设置、主题和用户交互。
  • Tauri Commands:提供 create_qrpoll_qr_statefetch_portfoliosave_notification_config 等命令。
  • Rust 业务层:负责养基宝签名请求、持仓聚合、收益曲线归一化、通知编排。
  • SQLite:保存 token、通知配置、推送节流状态。
  • 托盘/菜单栏:把"今日收益"这种高频信息放到更轻的位置。

桌面端的优势在于本地能力强,不需要部署服务;难点在于 Web 逻辑要在 Rust 中再实现一套。为了避免多端数据不一致,项目把净值字段优先级、当日收益计算、交易时段判断等逻辑都尽量与 Web 后端保持同构。

七、编辑器插件:Webview 只是 UI,真正请求发生在宿主进程

编辑器插件分为 VS Code/Cursor 扩展和 JetBrains 插件。两者 UI 都使用 React Webview,但宿主完全不同。

VS Code/Cursor 扩展是 TypeScript Extension Host + Webview。JetBrains 插件是 Kotlin Host + JCEF Webview。它们共同的原则是:Webview 负责展示,宿主进程负责网络、持久化和状态同步

在插件市场里,它作为一个独立扩展安装。用户搜索 fund-helper 后安装,后续在 VS Code、Cursor、Trae、CodeBuddy、Qoder 等兼容 VS Code 扩展生态的编辑器里使用。

VS Code/Cursor 扩展流程:

flowchart TD A[打开侧边栏/底部面板/状态栏入口] --> B[Extension activate] B --> C[创建 FundHelperController] C --> D[注册 WebviewViewProvider 和命令] D --> E[Webview 加载 React 静态资源] E --> F[Webview postMessage: boot] F --> G[Extension Host 读取 globalState] G --> H{是否有 token} H -->|没有| I[Webview 请求 startLogin] I --> J[Host 获取二维码并返回 qr 消息] J --> K[Webview 展示二维码并轮询] K --> L[Host 轮询养基宝扫码状态] L --> M[登录成功后保存 session] H -->|有| N[Host 拉取持仓快照] M --> N N --> O[Host 更新 lastSnapshot 和状态栏] O --> P[postMessage 给所有 Webview] P --> Q[React Webview 渲染持仓]

这里有一个重要原因:VS Code Webview 有严格 CSP,不能把它当普通网页随便直连外网。项目把养基宝请求放在 Extension Host 里,Webview 和 Host 之间只通过 postMessage 传递 bootstartLoginpollQrrefreshlogout 等消息。

登录后的编辑器端并不是只有一个侧边栏。项目把同一份 lastSnapshot 同步给侧边栏、底部面板、编辑器标题栏入口和状态栏,用户可以在不同工作流位置查看同一份持仓数据。

JetBrains 插件流程类似,只是桥接层换成 JCEF 和 Kotlin:

flowchart TD A[打开 JetBrains Tool Window] --> B[JCEF 加载 React Webview] B --> C[注入 JS Bridge] C --> D[Webview 发送 boot/startLogin/refresh] D --> E[Kotlin FundHelperController 接收消息] E --> F[SessionStorageService 读取登录态] F --> G{是否有 token} G -->|没有| H[YjbClient 获取二维码] H --> I[通知 Webview 展示二维码] I --> J[轮询扫码状态] J --> K[保存 token 到 PersistentStateComponent] G -->|有| L[PortfolioFetcher 拉取并聚合持仓] K --> L L --> M[更新 lastSnapshot] M --> N[通知 Tool Window / 底部面板 / 状态栏]

编辑器插件最大的挑战不是接口调用,而是宿主环境约束:

  • VS Code 需要处理 Webview CSP、资源 URI 重写、nonce、globalState 存储。
  • JetBrains 需要处理 JCEF 资源加载、JS bridge 注入、Tool Window 生命周期、状态栏 Widget。
  • 两端都要处理多个入口共享同一份快照:侧边栏、底部面板、状态栏不能各自乱刷新。

所以项目里有一个控制器角色:VS Code 是 FundHelperController,JetBrains 也是类似的 Controller。它们都负责保存 lastSnapshot、管理自动刷新、把结果广播给所有 UI 容器。

八、业务模型:多端可以不同,快照必须统一

这个项目里最有价值的部分之一,是对原始基金数据的归一化。

养基宝返回的基金净值字段并不总是稳定。例如部分 QDII、港股相关基金,常规字段 gszzlzsgzzl 可能为空,真实可用的估值涨跌幅在 vgszzl。如果客户端只读一个字段,很容易导致交易时段涨跌幅为空或收益估算错误。

项目里统一做了这样的优先级:

归一化目标 字段优先级
估算涨跌幅 gszzl -> zsgzzl -> vgszzl
公布涨跌幅 jzzzl -> rzzl
估算净值 gzjz -> zsgz -> gsz -> vgsz
当日收益 money * rate / 100

这类逻辑看起来不显眼,但它决定了数据是否可信。尤其是多端项目,如果 Web、浏览器插件、桌面端、IDE 扩展各算各的,很容易出现"同一个基金在不同端收益不一致"的问题。

因此项目把关键计算逻辑在各端保持对齐:

  • Web 后端:backend/app/yjb/calculator.py
  • 浏览器插件:chrome-extension/src/lib/portfolio.ts
  • VS Code 扩展:vscode-extension/src/portfolio.ts
  • 桌面端:desktop/src-tauri/src/portfolio.rs

这也是多端项目的一个经验:UI 可以因端而异,但业务计算必须尽量一致

九、收益曲线:接口只是入口,重点是账户维度和图表取舍

收益曲线这一块最初看起来只是"调一个接口然后画线",真正实现时才发现,关键不在接口本身,而在两个问题:如何拿到不同账户自己的曲线,以及如何在多端里用足够轻的方式展示。

实现流程如下:

flowchart LR A[持仓快照加载完成] --> B[提取账户 ID 列表] B --> C[请求汇总曲线] B --> D[请求各账户独立曲线] C --> E[normalizeIncomeLine] D --> F[normalizeIncomeLines] E --> G[页面/桌面端状态] F --> G G --> H[SVG 收益曲线] H --> I[Hover 显示时间和收益率] H --> J[按盈亏决定红绿线条]

这里有一个实测结论:如果只按单个账户参数去取,可能拿到的仍然是汇总曲线;要稳定拿到账户独立曲线,需要按账户 ID 数组去拉。这个结论最后沉淀到了 Web 后端、桌面端 Rust 和各端文档里。

图表层也做了克制。项目没有引入 ECharts,而是自研 SVG 曲线。原因是收益分时曲线的数据量有限,交互也明确:时间轴、收益率、红绿趋势、Hover 提示。自研 SVG 可以减少包体,尤其适合浏览器插件和编辑器 Webview 这种对启动速度敏感的容器。

十、通知推送:让收益状态主动找你

很多收益面板的问题是:你必须主动打开它。

Fund Helper 的通知模块希望把一部分信息变成主动推送。比如交易时段内每 15 分钟推送一次持仓收益,或者手动刷新后自动把最新收益发到飞书/钉钉群。

通知配置主要包括:

  • 总开关。
  • 推送频率:手动、1 分钟、5 分钟、15 分钟、30 分钟、60 分钟。
  • 是否仅交易时段推送。
  • 渠道配置:钉钉、飞书、企业微信。
  • 投递方式:Webhook 或企业应用。

推送内容不是简单一句"今天涨了",而是包括总收益、账户收益、Top 涨跌基金等信息。飞书还支持交互卡片,让通知更像一张轻量收益日报。

这里的设计重点是:通知不是独立系统,而是复用同一套持仓快照能力。推送前会拉取最新快照,模板层只负责把快照渲染成不同渠道能接受的格式。

十一、部署与工程化:把项目做成可使用的软件

项目不只关注业务代码,也补齐了使用和发布链路。

技术栈整体如下:

模块 技术
后端 Python 3.12、FastAPI、httpx、Motor、AKShare、bcrypt
Web 前端 React 19、TypeScript、Rsbuild、Ant Design、Sass
浏览器插件 CRXJS、Vite、React、Manifest V3
VS Code 扩展 Extension Host、WebviewView、React、Vite、esbuild
JetBrains 插件 Kotlin、JCEF、Gradle、React Webview
桌面端 Tauri v2、Rust、React、SQLite、Tailwind
部署 Docker、docker compose、MongoDB
文档 Rspress、GitHub Pages
包管理 pnpm workspace

部署上,Web 应用支持两种模式:

bash 复制代码
# 本地开发:MongoDB 单独跑,前后端分离
./dev-infra.sh
./start.sh

# Docker 一体部署:app + MongoDB
docker compose --profile full up -d --build

Docker 模式下,FastAPI 可以托管 Web 静态资源,用户访问 http://localhost:8080 即可使用。MongoDB 数据通过 volume 持久化,登录态、用户和通知配置不会随容器重建丢失。

发版方面,项目提供了多个脚本:

  • publish-chrome.sh
  • publish-vscode.sh
  • publish-jetbrains.sh
  • publish-desktop.sh
  • publish-image.sh

并配套 GitHub Actions 构建 Chrome 插件、VS Code 扩展、JetBrains 插件、桌面端和文档站。

十二、几个真正踩过的技术点

这类项目容易被低估的地方不在页面,而在不同运行环境的边界处理。下面这些是实现时真正花时间处理过的问题。

1. QR 登录不能只看"是否拿到二维码"

扫码登录有三个状态要处理:二维码创建、扫码确认、token 落库。浏览器插件、VS Code Webview、JetBrains JCEF、Tauri 桌面端都要走这个流程,但定时器和销毁逻辑完全不同。

插件端用 runIdRef 防止 React effect 重复触发后旧轮询继续写状态;VS Code 端由 Webview 发送 pollQr,真正轮询发生在 Extension Host;JetBrains 端要放到后台线程执行,再通过 listener 把结果推回 JCEF;桌面端则拆成 create_qrpoll_qr_statecomplete_qr_login 三个 Tauri command,成功后写 SQLite。

这个流程如果没有清理 timer,很容易出现两个问题:二维码刷新后旧轮询仍在请求,以及登录成功后多个端重复写 session。

2. Webview 不能当普通浏览器页面用

VS Code/Cursor 的 Webview 有 CSP 限制,外部请求不能直接从 Webview 发起,所以养基宝请求必须放在 Extension Host。构建产物也不能直接引用普通相对路径,需要通过 webview.asWebviewUri 重写资源 URL,并给脚本注入 nonce。

JetBrains 的 JCEF 又是另一套限制:静态资源要注册本地 handler,消息通信要靠 JS bridge,页面加载完成前发送消息可能丢失。因此 JetBrains 插件里做了 resyncPanel(),每次 Webview ready 后重新推一次 session、loading 和 lastSnapshot。

这也是为什么编辑器插件里一定要有 Controller,而不是让每个 Webview 自己拉数据。Controller 管网络、缓存、状态栏和广播,Webview 只负责展示。

3. 多入口共享快照,否则状态栏和面板会打架

编辑器插件有侧边栏、底部 Panel、编辑区按钮、状态栏入口。桌面端也有主窗口、系统托盘、macOS 菜单栏弹窗。多个入口如果各自刷新,会造成请求放大和状态不一致。

项目里用 lastSnapshot 做最近一次快照缓存:刷新时只由 Host/Rust 层拉取一次,然后广播给所有 UI。VS Code 里是 postAll() 推给所有 Webview;JetBrains 里是 listener set;桌面端则在 fetch_portfolio 成功后同步主窗口和菜单栏标题。

这个设计解决的不是性能问题,而是用户感知问题:状态栏显示的收益、底部面板里的收益、主窗口里的收益必须是同一份数据。

4. 上游字段不是稳定 schema,归一化要写成规则

基金净值字段有不少例外。比如常见基金可以读 gszzl,但 QDII 或港股相关基金可能只有 vgszzl;已公布涨跌幅又可能在 jzzzlrzzl。如果直接在 UI 里读字段,迟早会出现某些基金涨跌幅为空、排序错乱、当日收益为 0。

所以项目没有把字段判断散在组件里,而是先归一化:

text 复制代码
estimateRate = gszzl -> zsgzzl -> vgszzl
publishedRate = jzzzl -> rzzl
displayRate = estimateRate != 0 ? estimateRate : publishedRate
dayEarn = money * displayRate / 100

这套逻辑在 Python、TypeScript、Rust、Kotlin 中各有一份实现。虽然这不是最理想的复用方式,但它保证了 Web、插件、桌面端、编辑器插件展示出来的收益逻辑一致。

5. 收益曲线的账户维度来自实测,不是文档推导

收益曲线一开始按常规理解会尝试用单个 account_id 拉取,但实测发现这样可能拿到的仍是汇总曲线。最后稳定方案是按账户 ID 数组请求,再按返回的账户 key 取对应曲线。

图表层也没有上 ECharts。收益曲线只有分钟级点位、红绿趋势、Hover 提示和首尾时间轴,自研 SVG 更容易控制体积和样式,也更适合插件 Popup、VS Code Webview 这类受限容器。

6. 桌面端的 Tauri 边界要切干净

桌面端前端只做交互和展示,不直接持有 token,也不直接处理通知发送。React 通过 invoke 调 Rust command,Rust 层再统一读 SQLite、签名请求养基宝、组装快照、触发通知、同步菜单栏。

这样的边界让桌面端的主窗口、菜单栏弹窗、后台 scheduler 都能复用同一套 Rust 逻辑。否则很容易变成主窗口里写一套、托盘里再写一套,最后两边表现不一致。

结语

Fund Helper 一开始只是想解决"我想更方便地看基金收益"这个小问题,但做着做着,它变成了一个关于多端产品、第三方接口封装、数据归一化、通知推送和工程化发布的完整实践。

它的核心价值不是堆了多少技术,而是把一个日常场景拆成了几条可以落地的工程链路:

  • 上游签名、token、错误处理放在哪一层。
  • PortfolioSnapshot 如何在 Web、插件、桌面端、编辑器插件里保持一致。
  • Webview、Tauri、Chrome Popup 这些不同容器的边界怎么切。
  • 多入口场景下如何共享快照,避免状态栏、面板、主窗口各自为政。
  • Docker、GitHub Actions、安装包和文档站如何支撑项目真正被使用。

如果你对基金持仓面板、多端客户端、FastAPI BFF、Tauri、浏览器插件、IDE 插件这些方向感兴趣,可以看看项目源码和文档,也欢迎 Star、Issue 或一起交流。

项目地址:github.com/ChinaCarlos...

文档中心:chinacarlos.github.io/fund-helper...

下载 Releases:github.com/ChinaCarlos...

相关推荐
用户852495071841 小时前
从多页到 SPA:用 50 行代码理解前端路由的本质
前端
Zldaisy3d1 小时前
连续纤维增材制造的机翼已飞上天,复材打印在低空飞行器上还需翻过几道坎?
java·前端·数据库
__sjfzllv___1 小时前
在职前端Leader学习/转行 AI Agent -DAY24
前端
喜欢睡觉1 小时前
从"白一下"到"丝滑切换"——前端路由的进化史
前端
mqiqe1 小时前
构建企业级 AI 原生架构:LLM Gateway、RAG、Agent 与 MCP 的协同关系解析
人工智能·架构·gateway
qq_425516181 小时前
录音转文字工具免费下载:免费额度与功能限制对比
android·人工智能·智能手机·powerpoint
玉宇夕落1 小时前
React 多种路由学习:HashRouter 源码级剖析
前端
他们叫我秃子1 小时前
前端开发转 Go 全栈(三):从函数到 error,Go 连“失败”都要明确返回
前端·后端·go
智体工坊1 小时前
从零搭建 AI 模型中转网关:部署、渠道、定价、装修全记录
运维·服务器·人工智能·搜索引擎·自动化