财经网站源码的目录结构与核心功能拆解

最新新闻3小时前发布 jdkjadmin
0 00
财经网站源码的目录结构与核心功能拆解

财经网站源码的目录结构与核心功能拆解

财经网站看起来像是行情表格、新闻列表和几张走势图,真正的工程难点却藏在数据来源、更新时效、数字精度、权限控制和故障处理里。一个可维护的项目,不只是把页面拼出来,还要能回答:数据从哪里来、什么时候更新、展示的数字代表什么、接口出错时用户会看到什么。

本文以 Node.js、Express 和 PostgreSQL 为例,拆解一个财经信息网站的常见目录、数据模型与接口。代码示例用于构建信息展示类应用的骨架,不包含真实行情供应商接入,也不构成交易系统或投资建议。若要接入生产数据,必须按供应商授权和所在地适用规则处理。

一、先界定网站要做什么

“财经网站”并不是单一产品。开发前应把用途划清楚,不同用途对数据、权限和可靠性的要求差别很大。

  • 行情与资讯展示:展示股票、指数、外汇或商品的报价、历史数据、新闻和公告。关键要求是标注来源、时间、市场状态与延迟属性。
  • 财经内容管理:编辑发布文章、专题和数据解读。重点是内容审核、作者权限、版本记录和版权管理。
  • 投资研究工具:提供筛选、图表、指标或模拟组合。需要解释计算口径、数据缺失处理和模型局限,避免把估算结果说成确定结论。
  • 交易或账户服务:涉及资金、账户、下单、身份核验等。它不是普通内容网站的自然延伸,必须单独评估资质、监管要求、账户安全、交易风控和审计能力。本文的示例不适用于真实交易。

还要区分“数据展示”和“数据授权”。能够从公开页面看到某项行情,不代表可以抓取、存储、再分发或商业使用。应从有权提供数据的供应商获取授权,核对允许的市场、字段、用户范围、缓存期限及展示方式。不要把爬虫抓到的数据称作实时行情,也不要在没有依据时宣称数据具有某种准确率或更新频率。

二、推荐的目录结构

下面按职责拆分代码。初期可以把部分目录合并,等功能增长后再拆;关键不是目录越多越好,而是避免页面、数据库和业务规则全部挤进一个文件。

finance-site/
├── package.json
├── .env.example
├── src/
│   ├── app.js                 # 创建 Express 应用、挂载路由和中间件
│   ├── server.js              # 读取配置并启动服务
│   ├── config/
│   │   └── env.js             # 配置读取与校验
│   ├── routes/
│   │   ├── quotes.js          # 行情查询路由
│   │   └── articles.js        # 内容查询路由
│   ├── controllers/
│   │   └── quoteController.js # HTTP 参数解析、状态码和响应
│   ├── services/
│   │   └── quoteService.js    # 查询规则、业务编排
│   ├── repositories/
│   │   └── quoteRepository.js # 数据库访问
│   ├── db/
│   │   └── pool.js            # PostgreSQL 连接池
│   ├── middleware/
│   │   └── errorHandler.js    # 统一错误处理
│   ├── validators/
│   │   └── quoteQuery.js      # 输入校验
│   └── jobs/
│       └── refreshQuotes.js   # 后台更新任务入口
├── migrations/
│   └── 001_create_quotes.sql
├── public/
│   ├── index.html
│   └── app.js
└── tests/
    ├── quoteQuery.test.js
    └── quotes.test.js
  • routes:只负责把 URL 和 HTTP 方法映射到控制器,不应在路由文件里直接写复杂 SQL。
  • controllers:处理请求与响应,例如读取查询参数、返回状态码。它不应承担行情指标计算或供应商对接细节。
  • services:承载业务规则,例如检查品种是否可查询、组合数据来源和更新时间。
  • repositories:封装数据库读写,让业务代码不依赖具体 SQL 细节。
  • jobs:负责定时或队列任务。行情采集不能依赖用户打开页面时临时执行,否则访问高峰会放大供应商请求和响应延迟。
  • migrations:记录数据库结构变更。不要只在开发者本地手工改表,否则测试和部署环境容易不一致。

三、行情数据模型:先定义口径,再设计字段

报价不是一个孤立数字。至少要知道它属于哪个品种、来自哪个来源、对应哪个时间、是实时还是延迟、是否经过调整。历史行情还可能涉及时区、交易日、复权方式、停牌和缺失值。字段含义不明确,后续图表和统计就会出现“代码能跑、结果却说不清”的问题。

下面是 PostgreSQL 的简化表结构。金额类字段使用 NUMERIC,避免将金融数值存成浮点类型。精度和小数位应根据具体资产类别及供应商定义确定,不能一概假设所有市场都保留两位小数。

CREATE TABLE instruments (
  id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  symbol TEXT NOT NULL,
  market TEXT NOT NULL,
  name TEXT NOT NULL,
  currency CHAR(3),
  UNIQUE (market, symbol)
);

CREATE TABLE quotes (
  id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  instrument_id BIGINT NOT NULL REFERENCES instruments(id),
  price NUMERIC(24, 10) NOT NULL,
  change_value NUMERIC(24, 10),
  change_percent NUMERIC(12, 6),
  source TEXT NOT NULL,
  as_of TIMESTAMPTZ NOT NULL,
  received_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  is_delayed BOOLEAN NOT NULL DEFAULT TRUE
);

CREATE INDEX quotes_instrument_as_of_idx
  ON quotes (instrument_id, as_of DESC);

as_of表示报价对应的时间,received_at表示本系统收到数据的时间。两者分开记录,才能发现供应商延迟、网络积压或任务异常。is_delayed只是业务标记,必须依据数据合同和实际来源填充,不能为了页面好看固定写成 false。

数据库中的 NUMERIC 通常会以字符串形式进入 JavaScript 客户端,保留这种表示比直接转换成 Number 更稳妥。JavaScript 的二进制浮点数不能精确表示许多十进制小数;对外展示前可按产品定义格式化,对需要精确计算的业务则应使用经过审查的十进制定点方案,并明确舍入规则。

四、一个可运行的最小接口

先搭一个只读行情接口,验证目录划分和参数处理。此示例使用内存数组模拟数据,便于直接运行;重启后数据会恢复为初始值,也不具备真实行情含义。生产环境应将仓储层替换为授权数据源与数据库,并补上认证、限流、监控和数据质量检查。

创建 package.json:

{
  "name": "finance-site-demo",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "start": "node src/server.js"
  },
  "dependencies": {
    "express": "^4.21.2"
  }
}

创建 src/server.js:

import express from "express";

const app = express();
app.disable("x-powered-by");
app.use(express.json({ limit: "32kb" }));

// 仅用于演示接口结构,不是真实行情数据。
const quotes = [
  {
    symbol: "DEMO",
    market: "example",
    price: "123.4500",
    changePercent: "0.250000",
    source: "demo",
    asOf: new Date().toISOString(),
    delayed: true
  },
  {
    symbol: "SAMPLE",
    market: "example",
    price: "98.1200",
    changePercent: "-0.100000",
    source: "demo",
    asOf: new Date().toISOString(),
    delayed: true
  }
];

const allowedSortFields = new Set(["symbol", "market"]);

app.get("/api/quotes", (req, res) => {
  const rawLimit = req.query.limit ?? "20";
  const rawOffset = req.query.offset ?? "0";
  const sort = req.query.sort ?? "symbol";

  // 只接受十进制整数文本,避免空字符串、科学计数法等意外输入。
  if (!/^\d+$/.test(String(rawLimit)) ||
      !/^\d+$/.test(String(rawOffset))) {
    return res.status(400).json({ error: "limit 和 offset 必须是非负整数" });
  }

  const limit = Number(rawLimit);
  const offset = Number(rawOffset);

  // 这是本示例接口自行设定的上限,不是 Express 或 HTTP 的通用限制。
  if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100) {
    return res.status(400).json({ error: "limit 必须在 1 到 100 之间" });
  }
  if (!Number.isSafeInteger(offset) || offset > 100000) {
    return res.status(400).json({ error: "offset 必须在 0 到 100000 之间" });
  }
  if (typeof sort !== "string" || !allowedSortFields.has(sort)) {
    return res.status(400).json({ error: "不支持的排序字段" });
  }

  const data = [...quotes]
    .sort((a, b) => a[sort].localeCompare(b[sort]))
    .slice(offset, offset + limit);

  res.json({
    data,
    pagination: { limit, offset, returned: data.length }
  });
});

app.use((req, res) => {
  res.status(404).json({ error: "Not found" });
});

app.use((err, req, res, next) => {
  console.error(err);
  if (res.headersSent) return next(err);
  res.status(500).json({ error: "Internal server error" });
});

const port = Number(process.env.PORT ?? 3000);
app.listen(port, () => {
  console.log(`Demo server listening on port ${port}`);
});

在项目根目录运行:

npm install
npm start

访问 http://localhost:3000/api/quotes?limit=1&offset=0,会得到 JSON 结果。这里对 limit、offset 设定的是应用自身的输入范围:返回数不超过 100,偏移不超过 100000。它们不是语言、框架或协议规定的通用上限,真实产品应根据数据库容量、接口用途和性能测试调整。偏移分页在大数据集上可能越来越慢,可改用基于时间与唯一键的游标分页。

排序字段通过白名单限定,是因为 SQL 参数化查询通常只能安全绑定值,不能把列名作为普通参数绑定。接入数据库时,不要直接把用户传入的 sort 拼进 SQL;应把允许的字段映射到固定 SQL 片段,并仍然使用参数化查询传递筛选值。

五、数据库访问与分层替换

真实项目可以让路由保持稳定,只替换仓储实现。以 PostgreSQL 驱动 pg 为例,核心思路是查询值使用占位符;动态排序则从固定映射中选择,而不是信任用户输入。

const sortColumns = {
  symbol: "i.symbol",
  market: "i.market"
};

export async function listQuotes(pool, { limit, offset, sort }) {
  const column = sortColumns[sort];
  if (!column) throw new Error("Unsupported sort field");

  const query = `
    SELECT i.symbol, i.market, q.price::text AS price,
           q.change_percent::text AS "changePercent",
           q.source, q.as_of AS "asOf", q.is_delayed AS delayed
    FROM quotes q
    JOIN instruments i ON i.id = q.instrument_id
    ORDER BY ${column} ASC, q.as_of DESC
    LIMIT $1 OFFSET $2
  `;

  const result = await pool.query(query, [limit, offset]);
  return result.rows;
}

示例中的 column 来自静态白名单,因此字符串插值不会把未经校验的用户内容带入 SQL。LIMIT 和 OFFSET 则通过占位符传入。实际使用前还应在服务层处理默认品种、无数据返回、数据库超时和连接失败等情况,并避免把数据库原始错误内容返回给客户端。

六、前端展示:把来源和时间一起展示

行情数字脱离时间与来源,容易让用户误以为是当前报价。页面至少应在适当位置标记来源、报价时间和延迟状态。以下是浏览器端的简化示例,使用 textContent 更新文本,避免把接口返回值当作 HTML 执行。

async function loadQuotes() {
  const response = await fetch("/api/quotes?limit=20");
  if (!response.ok) {
    throw new Error(`Request failed: ${response.status}`);
  }

  const payload = await response.json();
  const list = document.querySelector("#quotes");
  list.replaceChildren();

  for (const quote of payload.data) {
    const item = document.createElement("li");
    const label = document.createElement("span");
    const meta = document.createElement("small");

    label.textContent = `${quote.market} ${quote.symbol}: ${quote.price}`;
    meta.textContent =
      `来源:${quote.source};报价时间:${quote.asOf}` +
      (quote.delayed ? ";延迟数据" : "");

    item.append(label, document.createTextNode(" "), meta);
    list.append(item);
  }
}

loadQuotes().catch((error) => {
  console.error(error);
  document.querySelector("#status").textContent = "行情暂时无法加载";
});

页面格式化应与字段语义匹配。价格、百分比和成交量不能一律使用相同的小数位;币种、市场时区和负数展示规则也应由产品口径决定。用户输入的文章标题、评论和搜索词同样不能未经转义就插入 innerHTML。使用模板引擎时,应确认默认转义行为及其例外。

七、资讯模块与内容管理

资讯模块通常需要文章、作者、分类、标签、发布时间、状态和来源等信息。最小的文章记录可包含标题、正文、摘要、作者、状态、创建时间和发布时间。公开查询只返回已发布内容;草稿和审核中内容必须由具备权限的管理接口读取。

内容编辑界面还要考虑富文本安全。富文本并不等于可信 HTML:如果允许粘贴 HTML,应在服务端使用成熟的 HTML 清洗策略限制标签、属性和 URL 协议;前端仅靠隐藏按钮或移除脚本标签不足以构成可靠防护。对转载内容还应记录授权、出处和编辑记录,不能用技术上的可访问性替代版权许可。

搜索功能要限制查询长度与返回条数,并对数据库进行适当索引。若使用全文检索,应明确分词、语言和相关性排序方式。不要将用户查询直接拼接进正则表达式或 SQL,也不要默认把搜索结果当成已经核实的事实。

八、后台数据更新与时效管理

行情采集适合由独立任务执行:任务从授权来源获取数据,校验字段和时间,写入原始记录或标准化记录,再由 API 提供查询。建议保留足够的来源信息和任务状态,便于排查供应商中断、重复数据、时间错位和异常跳价。采集失败时不应悄悄用旧值冒充新值。

每条记录应有可解释的时间戳。还需要明确服务器、供应商和市场时区之间的转换规则,数据库内部通常统一保存带时区的时间点,展示时再按用户或市场时区格式化。涉及交易日的逻辑不能仅靠“自然日”判断,节假日、临时休市和不同市场交易时段都需要相应日历数据。

任务调度要考虑重复执行和重试。写入操作最好具备幂等性,例如依据供应商记录标识或“品种加时间加来源”建立唯一约束。重试并不总是安全:对于重复扣款等有副作用的操作尤其如此,而本文的只读展示架构也不应被直接扩展成资金操作系统。

九、关键安全与运营边界

  • 输入校验不等于完整防护:参数范围校验能拒绝明显异常输入,但不能替代认证、授权、速率限制、审计、超时和容量管理。
  • 防 SQL 注入:查询值使用参数化接口;表名、列名、排序方向等结构部分使用固定白名单。不要用字符串拼接用户输入来生成 SQL。
  • 控制接口资源消耗:限制分页大小和正文长度,设置数据库查询超时;根据实际部署增加反向代理或应用层限流。单机示例没有实现限流,不应被描述为能抵御滥用或拒绝服务攻击。
  • 妥善管理密钥:数据库口令和供应商密钥放在受控的环境配置或密钥管理服务中,不提交到代码仓库,不发送到浏览器。对开发、测试和生产环境使用不同凭据。
  • 处理浏览器安全:按需配置 HTTPS、内容安全策略、跨域策略、Cookie 属性和 CSRF 防护。CORS 不是认证机制;允许某个来源跨域读取响应,也不等于该来源有权操作账户。
  • 日志避免泄露:记录请求标识、错误类别和必要的运行指标,不要把密码、访问令牌、完整个人信息或敏感密钥写进日志。
  • 识别系统边界:行情接口、资讯后台、用户账户和交易执行最好按不同权限与风险设计。只读页面的安全设计不能直接视为资金系统的安全设计。

十、常见错误及修正方式

  1. 把示例数字当成实时数据。静态样例只用于开发页面和接口。应清楚标注演示数据,并在生产接口中返回实际来源与时间。
  2. 用浮点数做所有财务计算。浮点表示可能产生小数误差。金额与报价优先采用数据库定点数或经过验证的十进制方案,定义精度、舍入和单位。
  3. 只保存价格,不保存时间和来源。这样无法判断数据是否过期,也难以追踪异常。至少区分数据对应时间、系统接收时间和数据来源。
  4. 把业务规则放在路由里。路由越写越长后,校验、查询和错误处理会重复。将 HTTP 处理、业务编排和数据访问分开,测试也更容易。
  5. 相信前端校验可以保护服务端。客户端校验是用户体验措施,请求可以被绕过。所有信任边界上的输入,都要由服务端重新校验。
  6. 用用户参数直接拼接 SQL。尤其是排序字段和过滤表达式。值使用占位符,结构使用白名单,并对排序、分页等参数设置合理范围。
  7. 无条件使用大偏移分页。数据量增长后,数据库可能需要扫描大量记录。热门列表或长历史查询可考虑游标分页、时间窗口和索引优化。
  8. 静默返回缓存旧值。缓存可以改善延迟,但必须返回可识别的更新时间或状态。过期数据不能伪装成最新数据。
  9. 把免责声明当作数据质量方案。免责声明不能弥补错误标注、错误计算、越权使用数据或不符合适用规则的问题。数据授权、口径说明和质量监控应分别落实。

十一、从原型到生产的实施顺序

  1. 先写数据字典:列明品种标识、币种、市场时区、来源、更新时间、延迟属性和价格精度。
  2. 用静态样例做页面:先验证列表、详情、加载状态、错误状态和移动端布局,不要过早接入未经确认的数据源。
  3. 明确数据授权和接口契约:确认可用字段、更新方式、调用频率、缓存和再分发限制,再设计采集任务。
  4. 建立数据库迁移与仓储层:为主要查询建立索引,设计唯一键、保留策略和备份恢复方案。
  5. 补齐服务端校验与测试:覆盖缺失参数、非法输入、超界分页、空结果、供应商故障和数据库超时。
  6. 上线前做运营验证:监控请求延迟、错误率、数据新鲜度和任务成功率;准备告警、回滚和故障说明流程。

结语

财经网站的目录结构,应围绕职责边界组织:路由处理 HTTP,服务层处理业务,仓储层访问数据,后台任务负责采集,前端准确呈现口径与状态。真正决定系统是否可靠的,往往不是页面用了什么框架,而是数据授权是否清楚、时间和精度是否定义一致、异常是否可见,以及安全控制是否落在服务端。

可以先用本文的只读示例搭建原型,再逐步接入经过授权的数据源和持久化数据库。每一步都保留来源、时间、精度和错误状态的解释空间,才能让源码不仅“能跑”,也便于维护和审计。

© 版权声明

相关文章