一、项目概述
OpenStock 是一个开源的股票市场交易平台替代品,旨在打破昂贵金融数据平台的垄断。该项目由 Open Dev Society 社区驱动,基于 AGPL-3.0 许可证发布,承诺永久免费且代码开放。
核心价值:
- 实时股价追踪与个性化告警
- 深度公司财务洞察与资讯聚合
- 基于 TradingView 的专业图表分析
- AI 驱动的个性化邮件服务
技术亮点:
- 现代全栈架构:Next.js 15 App Router + React 19 + TypeScript
- UI 系统:shadcn/ui + Radix UI + Tailwind CSS v4
- 数据层:MongoDB + Mongoose ODM
- 认证系统:Better Auth 集成 MongoDB Adapter
- 市场数据:Finnhub API(实时报价、公司档案、财经新闻)
- 可视化:TradingView 可嵌入 Widget
- 自动化:Inngest(事件驱动、定时任务、AI 推理)
- 邮件服务:Nodemailer(Gmail 传输)
二、技术原理
2.1 架构设计
OpenStock 采用 Next.js App Router 的现代路由架构,充分利用服务器组件(RSC)与服务端操作(Server Actions)的优势:
app/
├── (auth)/ # 认证相关页面(登录、注册)
│ ├── layout.tsx
│ ├── sign-in/
│ └── sign-up/
├── (root)/ # 主应用区域
│ ├── layout.tsx
│ ├── page.tsx # 仪表盘首页
│ ├── help/ # 帮助页面
│ └── stocks/
│ └── [symbol]/ # 动态股票详情页
├── api/
│ └── inngest/ # Inngest 事件接收端点
├── globals.css # 全局样式
└── layout.tsx # 根布局
关键设计决策:
- 路由分组:使用
(auth)和(root)进行逻辑隔离,便于布局管理和中间件保护 - 动态路由:
stocks/[symbol]实现股票详情页的动态生成 - API 路由:
/api/inngest作为 Inngest 的事件接收器,处理异步任务
2.2 认证与数据持久化
Better Auth + MongoDB Adapter 集成:
// lib/better-auth/...
// 认证配置示例(基于项目结构推断)
import { betterAuth } from "better-auth";
import { mongodbAdapter } from "better-auth/adapters/mongodb";
import mongoose from "mongoose";
export const auth = betterAuth({
database: mongodbAdapter(mongoose.connection.db),
emailAndPassword: {
enabled: true,
},
session: {
cookieCache: {
enabled: true,
maxAge: 5 * 60, // 5 minutes
},
},
});
中间件保护策略:
- Next.js middleware 验证会话有效性
- 未认证用户重定向至
/sign-in - 公共路径白名单:
/sign-in、/sign-up、/api/inngest、/_next/static
用户观察列表数据模型:
// database/models/watchlist.model.ts
import mongoose, { Schema, Document } from "mongoose";
export interface IWatchlist extends Document {
userId: string;
symbol: string;
addedAt: Date;
}
const WatchlistSchema = new Schema<IWatchlist>({
userId: { type: String, required: true },
symbol: { type: String, required: true },
addedAt: { type: Date, default: Date.now },
});
// 复合唯一索引:防止同一用户重复添加相同股票
WatchlistSchema.index({ userId: 1, symbol: 1 }, { unique: true });
export default mongoose.models.Watchlist
|| mongoose.model<IWatchlist>("Watchlist", WatchlistSchema);
2.3 市场数据集成
Finnhub API 集成模式:
// lib/actions/finnhub.ts (推断实现)
const FINNHUB_BASE_URL = process.env.FINNHUB_BASE_URL || "https://finnhub.io/api/v1";
export async function searchSymbols(query: string) {
const res = await fetch(
`${FINNHUB_BASE_URL}/search?q=${encodeURIComponent(query)}&token=${process.env.NEXT_PUBLIC_FINNHUB_API_KEY}`
);
return res.json();
}
export async function getCompanyProfile(symbol: string) {
const res = await fetch(
`${FINNHUB_BASE_URL}/stock/profile2?symbol=${symbol}&token=${process.env.NEXT_PUBLIC_FINNHUB_API_KEY}`
);
return res.json();
}
TradingView Widget 嵌入:
- 使用
next.config.ts的images.remotePatterns配置允许外部图片域名 - 在股票详情页嵌入 TradingView 的高级图表、技术指标、公司概况等 Widget
- 通过
i.ibb.co托管的部分静态资源同样需要配置域名白名单
2.4 自动化与 AI 集成
Inngest 事件驱动架构:
// lib/inngest/functions.ts (推断)
import { inngest } from "./client";
// 用户注册事件 → 触发 AI 个性化欢迎邮件
export const sendWelcomeEmail = inngest.createFunction(
{ id: "send-welcome-email" },
{ event: "app/user.created" },
async ({ event, step }) => {
const { userId, email, name, preferences } = event.data;
// 调用 Gemini API 生成个性化介绍
const personalizedIntro = await step.run("generate-ai-intro", async () => {
// 基于用户偏好(国家、投资目标、风险承受能力)生成文案
return await generateGeminiContent(preferences);
});
// 发送邮件
await step.run("send-email", async () => {
return await sendEmail({
to: email,
subject: `Welcome to OpenStock, ${name}!`,
html: `<p>${personalizedIntro}</p>`,
});
});
}
);
// 定时任务:每日财经摘要邮件(Cron 0 12 * * *)
export const dailyNewsSummary = inngest.createFunction(
{ id: "daily-news-summary" },
{ cron: "0 12 * * *" },
async ({ step }) => {
// 1. 获取所有用户
// 2. 遍历用户观察列表
// 3. 获取相关新闻
// 4. 发送个性化摘要
}
);
AI 提供商抽象:
- 支持 Gemini(默认)、MiniMax、Siray 等多种 AI 提供商
- 通过环境变量
AI_PROVIDER切换 - 统一的 Prompt 模板管理(
lib/inngest/prompts)
2.5 搜索与命令面板
Cmd/Cmd+K 全局搜索实现:
// components/SearchCommand.tsx (基于 cmdk 库)
import { Command } from "cmdk";
import { useDebounce } from "@/lib/utils";
export function SearchCommand() {
const [query, setQuery] = useState("");
const debouncedQuery = useDebounce(query, 300);
const [results, setResults] = useState([]);
useEffect(() => {
if (debouncedQuery.length < 2) return;
// 调用 Finnhub 搜索 API
fetch(`/api/search?q=${debouncedQuery}`)
.then(res => res.json())
.then(data => setResults(data.result || []));
}, [debouncedQuery]);
return (
<Command>
<Command.Input
placeholder="搜索股票代码或公司名称..."
value={query}
onValueChange={setQuery}
/>
<Command.List>
{results.map(stock => (
<Command.Item
key={stock.symbol}
onSelect={() => router.push(`/stocks/${stock.symbol}`)}
>
{stock.description} ({stock.symbol})
</Command.Item>
))}
</Command.List>
</Command>
);
}
三、安装与快速开始
3.1 环境要求
必需:
- Node.js 20+
- pnpm 或 npm
- MongoDB 实例(本地或 Atlas 云服务)
- Finnhub API Key(免费版支持基础功能)
- Gmail 账号(用于邮件发送,建议配置应用专用密码)
可选:
- Google Gemini API Key(AI 欢迎邮件功能)
- Adanos API Key(情绪分析功能)
- Inngest CLI(本地开发事件调试)
3.2 安装步骤
1. 克隆仓库:
git clone https://github.com/Open-Dev-Society/OpenStock.git
cd OpenStock
2. 安装依赖:
# 使用 pnpm(推荐)
pnpm install
# 或使用 npm
npm install
3. 环境配置:
创建 .env 文件(参考 Environment Variables 章节):
# 基础配置
NODE_ENV=development
# 数据库(本地 Docker)
MONGODB_URI=mongodb://root:example@mongodb:27017/openstock?authSource=admin
# 或 MongoDB Atlas
# MONGODB_URI=mongodb+srv://<user>:<pass>@<cluster>/<db>?retryWrites=true&w=majority
# Better Auth
BETTER_AUTH_SECRET=your_random_secret_string
BETTER_AUTH_URL=http://localhost:3000
# Finnhub API
NEXT_PUBLIC_FINNHUB_API_KEY=your_finnhub_key
FINNHUB_BASE_URL=https://finnhub.io/api/v1
# Gemini AI(可选)
GEMINI_API_KEY=your_gemini_api_key
# Inngest(Vercel 部署必需)
INNGEST_SIGNING_KEY=your_inngest_signing_key
# 邮件服务(Gmail)
NODEMAILER_EMAIL=[email protected]
NODEMAILER_PASSWORD=your_gmail_app_password
4. 验证数据库连接:
pnpm test:db
# 或
npm run test:db
3.3 最简运行示例
本地开发(Turbopack):
pnpm dev
# 访问 http://localhost:3000
启动 Inngest 本地开发服务器(新终端):
npx inngest-cli@latest dev
# Inngest 开发服务器运行在 http://localhost:8288
生产构建:
pnpm build && pnpm start
3.4 Docker Compose 部署
一键启动(推荐):
docker compose up -d mongodb && docker compose up -d --build
访问:
- 应用:http://localhost:3000
- MongoDB:mongodb:27017(容器内网络)
Docker Compose 配置要点:
mongodb服务使用官方mongo:7镜像- 配置健康检查:
mongosh --eval "db.adminCommand('ping')" - 数据持久化:使用命名卷
mongo-data - 应用服务依赖
mongodb,通过depend_on控制启动顺序 - 连接字符串必须使用
authSource=admin(root 用户认证)
四、使用方法与实战
4.1 基础用法
1. 用户注册与登录:
- 访问
/sign-up创建账号 - 填写国家、投资目标、风险承受能力、偏好行业等个性化信息
- Better Auth 处理会话管理,中间件保护私有路由
2. 搜索股票:
- 使用
Cmd/Ctrl + K打开命令面板 - 输入股票代码或公司名称(如 “AAPL” 或 “Apple”)
- Finnhub API 提供实时搜索结果
- 点击结果跳转至详情页
3. 管理观察列表:
- 在股票详情页点击 “添加到观察列表”
- 每个用户的观察列表独立存储(MongoDB)
- 防止重复添加(数据库唯一索引约束)
4.2 进阶用法
1. 深度公司分析:
- 访问
/stocks/AAPL(以苹果为例) - 查看 TradingView 高级图表(K 线、技术指标)
- 阅读公司档案(行业、市值、PE 比率等)
- 查看最新财经新闻(Finnhub 提供)
2. 市场概览:
- 首页展示市场热力图(TradingView Heatmap Widget)
- 实时报价列表(TradingView Quotes Widget)
- 头条新闻(TradingView News Widget)
3. 情绪分析(可选):
- 如果配置了
ADANOS_API_KEY - 股票详情页显示 Reddit、X.com、新闻、Polymarket 的综合情绪快照
- 辅助投资决策(非金融建议)
4.3 实际项目示例
场景:构建个人股票监控面板
// 自定义组件:用户观察列表实时监控
// components/WatchlistMonitor.tsx
import { useEffect, useState } from "react";
import { getWatchlist, getQuote } from "@/lib/actions";
export function WatchlistMonitor() {
const [watchlist, setWatchlist] = useState([]);
const [quotes, setQuotes] = useState({});
useEffect(() => {
async function fetchData() {
// 1. 获取用户观察列表
const symbols = await getWatchlist();
setWatchlist(symbols);
// 2. 批量获取实时报价
const quotesData = {};
for (const item of symbols) {
const quote = await getQuote(item.symbol);
quotesData[item.symbol] = quote;
}
setQuotes(quotesData);
}
fetchData();
// 3. 定时刷新(每 60 秒)
const interval = setInterval(fetchData, 60000);
return () => clearInterval(interval);
}, []);
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{watchlist.map(item => (
<Card key={item.symbol}>
<CardHeader>
<CardTitle>{item.symbol}</CardTitle>
</CardHeader>
<CardContent>
<p>当前价: ${quotes[item.symbol]?.c}</p>
<p className={quotes[item.symbol]?.d > 0 ? "text-green-500" : "text-red-500"}>
涨跌: {quotes[item.symbol]?.d} ({quotes[item.symbol]?.dp}%)
</p>
</CardContent>
</Card>
))}
</div>
);
}
性能优化建议:
- 使用 React 19 的
<Suspense>和流式 SSR - 报价数据使用 SWR 或 React Query 缓存
- TradingView Widget 使用懒加载(Intersection Observer)
- 考虑 WebSocket 推送实时数据(Finnhub 支持)
五、常见问题与解决方案
5.1 安装失败
问题:pnpm install 报错 “ENOMEM: not enough memory”
解决方案:
# 增加 Node.js 内存限制
export NODE_OPTIONS="--max-old-space-size=4096"
pnpm install
问题:MongoDB 连接失败(Docker Compose 环境)
解决方案:
- 检查 MongoDB 容器健康状态:
docker ps docker logs mongodb - 确认
.env中连接字符串正确:MONGODB_URI=mongodb://root:example@mongodb:27017/openstock?authSource=admin- 注意:使用服务名
mongodb(Docker Compose 网络内) - 必须包含
authSource=admin(root 用户认证)
- 注意:使用服务名
- 如果本地开发不使用 Docker,改为本地 MongoDB 或 Atlas 连接字符串
5.2 运行时错误
问题:Finnhub API 返回 401 Unauthorized
解决方案:
- 确认
.env中NEXT_PUBLIC_FINNHUB_API_KEY已设置 - 注意:
NEXT_PUBLIC_前缀会暴露给浏览器,建议仅用于公开数据 - 服务器端 API 调用使用单独的
FINNHUB_API_KEY(无NEXT_PUBLIC_前缀) - 免费版 Finnhub API 有速率限制(60 calls/min),生产环境建议升级付费版
问题:TradingView Widget 无法加载(跨域错误)
解决方案:
- 检查
next.config.ts的images.remotePatterns配置:images: { remotePatterns: [ { protocol: 'https', hostname: 'i.ibb.co', port: '', pathname: '/**', }, // 如果 Widget 使用其他 CDN,也需要添加 ], }, - 如果使用自定义域名托管 Widget 脚本,确保 HTTPS 配置正确
问题:Inngest 函数未触发
解决方案:
- 确认 Inngest CLI 正在运行(
npx inngest-cli@latest dev) - 检查
.env中INNGEST_SIGNING_KEY配置 - 查看 Inngest 开发控制台:http://localhost:8288
- 手动触发事件测试:
curl -X POST http://localhost:3000/api/inngest \ -H "Content-Type: application/json" \ -d '{"event": "app/user.created", "data": {...}}'
5.3 性能问题
问题:搜索输入卡顿(Cmd+K 命令面板)
解决方案:
- 已实现防抖(debounce)处理(300ms 延迟)
- 如果仍卡顿,增加防抖时间至 500ms
- 限制搜索结果数量(前端分页或后端限制返回 10 条)
- 使用 React 19 的
<Suspense>分包加载搜索结果
问题:MongoDB 查询慢(观察列表加载延迟)
解决方案:
- 确保
userId和symbol字段已建立索引:WatchlistSchema.index({ userId: 1, symbol: 1 }, { unique: true }); - 使用
mongoose.connect()的连接池配置:mongoose.connect(uri, { maxPoolSize: 10, // 调整连接池大小 serverSelectionTimeoutMS: 5000, }); - 考虑对热点数据使用 Redis 缓存(如用户观察列表)
5.4 兼容性问题
问题:Tailwind CSS v4 类名不生效
解决方案:
- 确认
postcss.config.mjs配置正确:const config = { plugins: ["@tailwindcss/postcss"], }; export default config; - 避免使用已弃用的 Tailwind v3 类名(如
text-opacity-*) - 查看 Tailwind CSS v4 迁移指南:https://tailwindcss.com/docs/upgrade-guide
问题:Better Auth 与 MongoDB Adapter 版本不兼容
解决方案:
- 锁定依赖版本:
{ "dependencies": { "better-auth": "1.3.25", "@better-auth/mongodb-adapter": "1.3.25" } } - 查看 Better Auth 官方文档的 MongoDB 适配指南
- 如果问题持续,切换至 Prisma + MongoDB 方案
六、总结
OpenStock 是一个技术栈现代、架构清晰、可扩展性强的开源股票交易平台。其核心价值在于:
技术亮点:
- 全类型安全:TypeScript 全覆盖,Mongoose ODM 提供 Schema 验证
- 现代化 UI:shadcn/ui + Radix UI 提供无障碍、可定制的组件库
- 实时数据:Finnhub + TradingView 组合提供专业级市场数据可视化
- 事件驱动:Inngest 简化异步任务、定时任务和 AI 推理流程
- AI 集成:Gemini API 实现个性化邮件内容生成
部署灵活性:
- 支持传统 Node.js 部署(Vercel、Railway、自托管)
- Docker Compose 一键启动(含 MongoDB)
- 环境变量驱动配置,易于迁移和扩展
社区与生态:
- Open Dev Society 社区活跃,欢迎新手贡献
- AGPL-3.0 许可证保证代码永久开放
- 完善的文档和 Issue 模板降低贡献门槛
适用场景:
- 个人股票监控工具(替代付费 App)
- 金融数据分析学习项目
- 全栈 Next.js 实战案例研究
- 开源社区协作示范项目
注意事项:
- 市场数据可能有延迟(取决于 Finnhub API 版本)
- 不构成金融投资建议
- 修改并部署需遵守 AGPL-3.0 许可证(开源衍生代码)
未来展望:
- 支持更多数据源(Yahoo Finance、Alpha Vantage)
- 移动端适配(React Native 或 PWA)
- 社交功能(用户分享观察列表、投资回报率排行榜)
- 多语言国际化(i18n)
OpenStock 证明了高质量金融工具可以是开源且免费的。对于开发者,它是学习现代 Next.js 全栈架构的优秀参考;对于用户,它是摆脱昂贵订阅的真正选择。
项目链接:
- GitHub:https://github.com/Open-Dev-Society/OpenStock
- 许可证:AGPL-3.0
- 在线演示:待部署(可本地运行)
参考资料:
- Next.js 官方文档:https://nextjs.org/docs
- Better Auth 文档:https://www.better-auth.com
- Finnhub API 文档:https://finnhub.io/docs/api
- TradingView Widget 文档:https://www.tradingview.com/widget-docs/
- Inngest 文档:https://www.inngest.com/docs