汇率

资金报表会把金额汇总到同一种货币,即报告货币。其他货币的金额按每日汇率换算。Specivo 完成这一步不需要联网:管理员可以手动录入汇率,也可以从 CSV 文件导入。每天自动获取欧洲央行(ECB)参考汇率是可选功能,默认关闭,需要管理员手动开启。

打开 管理 → 汇率。

汇率的含义

一条汇率的含义是 1 base = rate quote,例如 2026-11-05 这天 1 USD = 32.45 THB。汇率按天记录,每条汇率只属于一个日期。Specivo 按录入时的方向保存货币对,并自行推导反向汇率(1 THB = 1 / 32.45 USD),所以不需要两个方向都录入。

每条汇率都有一个来源:

来源 从哪里来
手动 在管理页面或通过 API 录入
CSV 导入 从文件导入
ECB 从 ECB 获取,或从你指定给 Specivo 的数据源获取

金额如何换算

假设要把一笔日期为 2026-11-05 的 THB 金额换算成 USD 来报告:

  1. 选哪一天的汇率。 Specivo 取金额日期当天或之前最新的一条汇率,但这条汇率最多只能是 7 天前的(即最长有效期,可在下文的设置里修改)。周末和银行假日由此得到覆盖:周六的金额使用周五的汇率。
  2. 选哪个货币对。 先找货币对本身(THB → USD),再找反向货币对(USD → THB,按 1 / rate 使用)。两者都没有可用汇率时,就经由中间货币(默认 EUR)换算:THB → EUR → USD。每一段都遵循同样的规则。
  3. 选哪个来源。 日期最新的优先。同一天的汇率中,手动录入的优先于导入的,导入的优先于自动获取的。因此要修正一条自动获取的汇率,只需为那一天添加一条手动汇率。

找不到汇率时,这笔金额不会被换算。报表会把这类金额按原币种单独列在合计旁边,并注明原因:“无汇率”(该日期当天及之前都没有汇率)或“已过期”(最新的汇率超过了最长有效期)。这些金额既不会被丢弃,也不会按零计算。

报表也可以改用最新汇率(现有最新的一条,不论日期),或者对所有金额统一使用某个固定日期的汇率。报表会显示实际使用的汇率日期。

汇率和金额都是精确的十进制数,从不使用浮点数。汇率最多保留 12 位小数。换算后的金额保留 8 位小数,只在显示时才按该货币自身的小数位数四舍五入。

试算换算

试算换算 卡片用已保存的汇率换算某一天的一笔金额,并显示所用的汇率、汇率日期,以及是否经过了反向或中间货币换算。无法换算时,它会说明原因。

手动录入汇率

在 添加汇率 中选择日期和两种货币,然后输入汇率,小数点用点号(36.5123)。两种货币都必须已在货币页面启用。同一日期和货币对只能有一条手动汇率;要修改它,在列表中点击 编辑。

只有手动汇率可以编辑。导入和自动获取的汇率可以删除,同一天的手动汇率会覆盖它们。被删除的自动获取汇率,会在下次获取那一天的数据时重新出现。

导入 CSV 文件

在 导入 CSV 文件 中上传文件或粘贴内容,然后点击 预览。预览会列出导入对每一行的处理结果(新增、修改已导入的汇率、不变),以及每个问题所在的行号。点击 导入 之前不会写入任何数据。

date,base,quote,rate
2026-11-04,USD,THB,32.40
2026-11-05,USD,THB,32.45
2026-11-05,EUR,THB,37.533
  • 每行一条汇率:date,base,quote,rate。标题行可有可无;空行和以 # 开头的行会被跳过。
  • 日期格式为 YYYY-MM-DD。货币代码是已启用货币的 ISO 4217 代码,不区分大小写。汇率必须为正数,使用点号作小数点,最多 12 位小数,不能带千位分隔符。
  • 同一日期和货币对在一个文件中只能出现一次。
  • 全部成功或全部不导入:只要有一行有问题,就什么都不会导入。修正文件后重新导入即可。
  • 再次导入同一日期和货币对会替换已导入的汇率;手动和自动获取的汇率不受影响。重复导入同一个文件是安全的。
  • 每个文件最大 2 MB、最多 20,000 行。更长的历史数据请拆分成多个文件。

电子表格软件可以直接导出这种格式。ECB 以 CSV 和 XML 形式发布完整历史汇率,你可以把它转换成这种格式来补录往年数据。

自动获取 ECB 汇率(可选)

自动获取默认关闭。在你开启之前,不会向互联网发出任何请求。

在 换算设置 中勾选 每天获取一次汇率,然后点击 保存。此后每天 16:15 UTC(ECB 在工作日约 16:00 CET 发布之后),Celery worker 会下载 ECB 欧元参考汇率(约 30 种货币对欧元的汇率,包括 USD、JPY、GBP、CNY、INR 和 THB),并以 ECB 为来源保存。当天已保存的汇率会被更新。立即获取 会马上执行一次获取,即使每日获取处于关闭状态也可以,这是测试连接的快捷办法。

卡片会显示上次获取的结果。获取失败不会重试,要等到第二天;7 天的最长有效期足以覆盖少数几天的缺失。

网络。 每日请求由 worker(celery-worker)发出,立即获取 的请求则由 api 容器发出。两者都需要能通过 HTTPS 访问 www.ecb.europa.eu。如果在代理后面,给两个容器都设置 HTTPS_PROXY(内部主机再加上 NO_PROXY),例如写在 .env 里,自带的 Compose 文件会把它们传给每个容器;获取功能会遵循这些设置,和语义搜索的模型下载一样。没有网络时,获取会失败并提示“无法连接数据源”,其他功能不受影响。

来源地址。 要使用 ECB 的每日文件,来源地址 留空即可。你也可以把它指向:

  • 另一个 ECB 文件,例如 https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist-90d.xml(最近 90 天,适合一次性补齐缺口);
  • 自托管的 Frankfurter 实例(MIT 许可证),它以 JSON 提供同样的 ECB 数据:你服务器上的 /v1/latest 或 /v2/rates?providers=ecb。

Specivo 能读取 ECB 的 XML 以及 Frankfurter v1 和 v2 的 JSON 格式,编码须为 UTF-8。地址必须以 http:// 或 https:// 开头。

获取功能允许访问哪些地址。 每个请求和每次重定向(最多三次)都会按实际目标检查:先解析主机名,逐个检查解析出的地址,然后只连接到已通过检查的地址。

  • 链路本地地址和云元数据地址(如 169.254.169.254)一律拒绝。
  • 环回地址和私有地址(127.0.0.1、10.x、172.16-31.x、192.168.x、fd00::/8)也会被拒绝,除非运维人员在 .env 中设置 EXCHANGE_RATE_FEED_ALLOW_PRIVATE_NETWORKS=true。如果你的 Frankfurter 实例运行在内网,就需要设置它。这个开关有意设计为环境变量:管理页面无法打开内网地址。
  • 非常规写法的数字主机名(0x7f.1、2130706433)会被拒绝。
  • 在代理(HTTPS_PROXY)后面时,每次重定向都会重新选择代理,并且目标主机会先在服务器上解析和检查,然后请求才发往代理。在服务器上无法解析的主机会被拒绝,因为无法对它进行检查。如果你的服务器只能通过代理解析域名,在 .env 中设置 EXCHANGE_RATE_FEED_ALLOW_UNRESOLVED_VIA_PROXY=true。

使用代理时,必须由代理执行出站规则

代理会自行再解析一次主机名,所以如果某个域名第二次解析出不同的结果(DNS 重绑定),Specivo 的检查拦不住它;而设置了 EXCHANGE_RATE_FEED_ALLOW_UNRESOLVED_VIA_PROXY 后,服务器上更是完全不做检查。配置了代理时,除非你确实想放行,否则代理必须拒绝连接到私有和链路本地地址段 (10.0.0.0/8、172.16.0.0/12、192.168.0.0/16、127.0.0.0/8、169.254.0.0/16、fc00::/7、 fe80::/10)。

  • 整个获取过程(包括下载)必须在 20 秒内完成,文件最大 8 MB。

管理页面上的错误信息会说明出了什么问题(“数据源返回错误”“第 3 项汇率无效”),但绝不会原样转述服务器返回的内容。

ECB 汇率的使用条款

ECB 发布欧元参考汇率仅供参考,并强烈不建议用于交易。它们适合成本和支出报表,这也正是 Specivo 使用它们的用途,而不适合开票或付款。

ECB 的版权条款允许免费使用其网站上的信息,条件是:

  • 注明 ECB 为来源,并准确转载信息。管理页面会把 ECB 标注为自动获取汇率的来源;
  • 对信息的任何修改都要说明。Specivo 原样保存发布的汇率,只在换算时推导反向汇率和(经中间货币的)交叉汇率,换算预览和报表会标注为“反向”或“经 EUR”;
  • 如果你转售含有这些汇率的文档,必须告知买家这些汇率可以免费获取。

Frankfurter 是 MIT 许可的软件,并不拥有数据:数据提供方(这里是 ECB)的条款才适用。以上条款核对于 2026 年 11 月;如果你依赖这些汇率,请自行查看 ECB 的页面。

离线部署

保持自动获取关闭。手动录入你需要的少量汇率,或者定期(例如每月)导入 CSV 文件。要把 ECB 汇率导入离线环境,在联网的机器上下载 https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist.zip,把需要的行转换成 date,base,quote,rate 格式(base 为 EUR),随其他传输数据一起带入内网后导入。如果两次导入之间的间隔超过了报表所覆盖天数对应的最长有效期,请在 换算设置 中调高最长有效期,而不是让金额留着不换算。

另见离线部署。

换算设置

设置 默认值 作用
中间货币 EUR 两种货币之间没有直接汇率时使用的货币。使用 ECB 汇率时请保持 EUR,因为 ECB 的汇率都是对欧元的。
汇率最长有效期 7 天 金额可使用的最旧汇率;0 表示只用同一天的汇率。
每天获取一次汇率 关闭 可选的每日获取。
来源地址 ECB 每日文件 获取功能的读取地址。

API

所有端点都仅限管理员使用。汇率是十进制字符串("32.45"),绝不是 JSON 数字。

端点 说明
GET /api/v1/admin/exchange-rates/ 汇率列表,日期最新的在前。筛选参数:base、quote、currency(任一侧)、source、date_from、date_to;分页:offset、limit。
POST /api/v1/admin/exchange-rates/ 添加手动汇率:{"rate_date": "2026-11-05", "base": "USD", "quote": "THB", "rate": "32.45"}。同一日期和货币对已有手动汇率时返回 409 exchange_rate_exists。
GET / PATCH / DELETE /api/v1/admin/exchange-rates/{id}/ 读取、编辑(仅限手动汇率,否则返回 422 exchange_rate_not_editable)或删除一条汇率。
POST /api/v1/admin/exchange-rates/import/ {"csv": "...", "dry_run": true}。返回各项计数、预览以及每个错误及其行号;有错误且不是试运行时返回 422。
GET / PATCH /api/v1/admin/exchange-rates/settings/ pivot、max_age_days、fetch_enabled、fetch_url,以及 last_fetch(只读)。
POST /api/v1/admin/exchange-rates/fetch/ 立即获取。失败会在响应体中报告(ok: false、error)。
POST /api/v1/admin/exchange-rates/convert/ {"money": {"amount": "100", "currency": "USD"}, "to": "THB", "on": "2026-11-05", "policy": "entry_date"}。policy 可以是 entry_date(默认)、latest 或一个日期。返回 status: converted 以及金额和所用汇率,或返回 status: unconverted 以及 reason(no_rate 或 stale)。

通用设置端点(PATCH /api/v1/admin/settings/)会拒绝汇率设置的键;请使用上面的端点。

添加、编辑、删除和导入汇率,开关自动获取,以及修改设置,都会写入安全审计日志。