Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

RustBill

基于 Rust 的财务软件,用于分销云服务器等商品。支持多租户、多供应商、多支付网关、多通知渠道。

核心能力

  • 商品管理 — 动态规格、多级定价、分组分类
  • 订单系统 — 创建→支付→开通→通知全生命周期
  • 插件系统 — Rune 脚本热插拔,零编译部署
  • 高可用 — 水平扩展 + Citus 分布式 + 熔断降级
  • 双前端 — 管理后台 (SPA) + 客户前台 (shadcn/ui)

快速开始

bash <(curl -fsSL https://raw.githubusercontent.com/zyxisme/rustbill-releases/main/deploy.sh)

详见 快速部署指南

快速部署指南

本指南帮助你在 10 分钟内将 RustBill 部署到一台全新的 Ubuntu 服务器(20.04 / 22.04 / 24.04)。


1. 二进制部署(推荐)

RustBill 提供交互式部署脚本,一行命令完成全部初始化:

bash <(curl -fsSL https://raw.githubusercontent.com/zyxisme/rustbill-releases/main/deploy.sh)

脚本会自动完成以下步骤:

  • 检测系统类型和架构,下载对应的预编译二进制
  • 生成默认配置文件 config.toml
  • 创建 systemd 服务单元
  • 初始化 PostgreSQL 数据库(可选)
  • 配置防火墙规则

非交互模式(CI / 自动化脚本):

bash <(curl -fsSL https://raw.githubusercontent.com/zyxisme/rustbill-releases/main/deploy.sh) -- -y

脚本执行完毕后,二进制位于 /usr/local/bin/rustbill-server,配置文件位于 /etc/rustbill/config.toml

手动下载

如需手动安装,从 GitHub Releases 下载对应平台的 tar.gz,解压后将二进制放入 /usr/local/bin/

# 示例:x86_64 Linux
curl -L -o rustbill.tar.gz https://github.com/zyxisme/rustbill-releases/releases/latest/download/rustbill-x86_64-unknown-linux-gnu.tar.gz
tar xzf rustbill.tar.gz
sudo mv rustbill-server rustbill-cli /usr/local/bin/

2. PostgreSQL 安装

RustBill 需要 PostgreSQL 16+。

Ubuntu 22.04 / 24.04

# 安装
sudo apt update && sudo apt install -y postgresql

# 启动并设为开机自启
sudo systemctl enable --now postgresql

创建用户和数据库

sudo -u postgres psql <<EOF
CREATE USER rustbill WITH PASSWORD 'your-db-password';
CREATE DATABASE rustbill OWNER rustbill;
GRANT ALL PRIVILEGES ON DATABASE rustbill TO rustbill;
EOF

注意:将 your-db-password 替换为强密码。RustBill 启动时会自动执行数据库迁移(创建表结构),无需手动运行 SQL 文件。


3. 最小配置

RustBill 默认读取 /etc/rustbill/config.toml,并可选叠加 /etc/rustbill/config.local.toml(Figment 层叠合并)。

配置文件路径

路径用途
/etc/rustbill/config.toml主配置文件
/etc/rustbill/config.local.toml本地覆盖(不纳入版本控制)
./plugins/Rune 插件脚本目录

最小化 config.toml

创建 /etc/rustbill/config.toml,写入以下内容(仅 3 个必填项):

sudo mkdir -p /etc/rustbill /etc/rustbill/plugins

# 生成 JWT secret
JWT_SECRET=$(openssl rand -base64 64)

sudo tee /etc/rustbill/config.toml > /dev/null <<EOF
[server]
host = "127.0.0.1"
port = 50051

[db]
url = "postgres://rustbill:your-db-password@localhost/rustbill"

[jwt]
secret = "$JWT_SECRET"

[bootstrap]
admin_password = "your-admin-password"
EOF

必须修改的字段

  • [db].url — 将 your-db-password 替换为上一步创建的数据库密码
  • [bootstrap].admin_password — 替换为管理员初始密码(首次登录后建议修改)

完整配置参考

更多配置项(Redis、速率限制、CORS、Session 等)参见 config.example.toml

curl -fsSL https://raw.githubusercontent.com/zyxisme/rustbill/refs/heads/main/config.example.toml

4. 启动服务

创建 systemd 服务

sudo tee /etc/systemd/system/rustbill-server.service > /dev/null <<'EOF'
[Unit]
Description=RustBill Server
After=network.target postgresql.service
Requires=postgresql.service

[Service]
Type=simple
User=rustbill
Group=rustbill
ExecStart=/usr/local/bin/rustbill-server
WorkingDirectory=/etc/rustbill
Restart=always
RestartSec=5
Environment=RUST_LOG=rustbill_server=info

# 安全加固
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/etc/rustbill
ReadOnlyPaths=/usr/local/bin

[Install]
WantedBy=multi-user.target
EOF

创建运行用户并设置权限

sudo useradd -r -s /bin/false -d /etc/rustbill rustbill
sudo chown -R rustbill:rustbill /etc/rustbill

启动并验证

# 重载 systemd,启动服务
sudo systemctl daemon-reload
sudo systemctl enable --now rustbill-server

# 检查状态
sudo systemctl status rustbill-server

# 查看日志
sudo journalctl -u rustbill-server -f

健康检查

curl http://127.0.0.1:50051/health
# 预期输出:OK

5. Caddy 反代(推荐)

Caddy 原生支持 HTTP/3(QUIC)和自动 Let’s Encrypt 证书,是 RustBill 推荐的反向代理。

安装 Caddy

sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update && sudo apt install -y caddy

最小 Caddyfile

创建 /etc/caddy/Caddyfile(将 your-domain.com 替换为你的域名):

your-domain.com {
    # gRPC 后端(支持 gRPC-Web)
    handle /rustbill.* {
        reverse_proxy 127.0.0.1:50051
    }

    # 健康检查直通
    handle /health {
        reverse_proxy 127.0.0.1:50051
    }

    # Admin SPA(内嵌于 RustBill 二进制)
    handle /admin* {
        reverse_proxy 127.0.0.1:50051
    }

    # Customer 前台(独立静态文件部署)
    handle {
        root * /var/www/rustbill-consumer
        try_files {path} /index.html
        file_server
    }
}

Caddy 自动申请 Let’s Encrypt 证书,启用 HTTP/3(QUIC)。唯一前提是域名 DNS 已指向服务器 IP。

重载配置

sudo systemctl reload caddy

6. 前端部署

Admin 管理后台

Admin SPA 已内嵌在 rustbill-server 二进制中,无需额外部署。 浏览器访问 https://your-domain.com/admin 即可使用。

如修改了 [server].admin_path 配置项,访问路径需同步调整。

Customer 前台

Customer SPA 是独立的前端应用,需单独构建和部署:

# 从 GitHub Releases 下载预构建的静态文件
curl -L -o consumer-dist.tar.gz https://github.com/zyxisme/rustbill-releases/releases/latest/download/consumer-dist.tar.gz

sudo mkdir -p /var/www/rustbill-consumer
sudo tar xzf consumer-dist.tar.gz -C /var/www/rustbill-consumer
sudo chown -R caddy:caddy /var/www/rustbill-consumer

配置 gRPC 端点

Customer SPA 运行时从 config.json 读取后端地址。编辑 /var/www/rustbill-consumer/config.json

{
  "grpcEndpoint": "https://your-domain.com",
  "appTitle": "RustBill Cloud",
  "adminUrl": "https://your-domain.com/admin"
}

字段说明

  • grpcEndpoint — gRPC 服务的完整 origin(含 https://),Customer SPA 通过此地址发起 gRPC-Web 请求
  • appTitle — 页面标题
  • adminUrl — 管理后台跳转链接(前台登录页展示)

本地构建(可选)

如需自定义主题/品牌,从源码构建:

git clone https://github.com/zyxisme/rustbill.git
cd rustbill/web-consumer
npm install
cp brand.yaml.example brand.yaml    # 按需编辑品牌配置
npm run build
# 产物在 dist/ 目录

7. 首次登录

  1. 浏览器打开 https://your-domain.com/admin
  2. 使用 [bootstrap].admin_password 中设置的用户名和密码登录
    • 默认用户名:admin
    • 密码:配置文件中 admin_password 的值
  3. 登录后建议立即修改密码:
    • 导航到 系统管理 → 用户管理
    • 点击当前用户,修改密码

首次登录后,你可以在 Admin 后台完成以下操作:

  • 添加商品 — 商品管理 → 新建商品
  • 配置插件 — 基础设施 → 插件管理 → 创建接口实例
  • 管理客户 — 客户管理 → 新建客户

8. 验证清单

部署完成后,逐项验证以下功能正常:

检查项命令 / 操作预期结果
服务运行sudo systemctl status rustbill-serveractive (running)
健康检查curl http://127.0.0.1:50051/healthOK
数据库连接sudo journalctl -u rustbill-server | grep -i "database"无连接错误
HTTPS 可访问浏览器打开 https://your-domain.com/health显示 OK,证书有效
Admin 登录浏览器打开 https://your-domain.com/admin显示登录页,可成功登录
Customer 前台浏览器打开 https://your-domain.com显示产品首页,可浏览目录
gRPC 通信Admin 后台 → Dashboard显示统计数据(非白屏)
插件扫描sudo journalctl -u rustbill-server | grep -i "plugin"显示已扫描的插件列表

常见问题

服务启动失败

# 查看详细日志
sudo journalctl -u rustbill-server -n 50 --no-pager

常见原因:

  • 数据库连接失败 — 检查 config.toml[db].url 是否正确,PostgreSQL 是否运行
  • JWT secret 为空[jwt].secret 必须设置且不能为 "change-me-in-production"
  • 插件目录不可写 — 确保 plugins/ 目录存在且 rustbill 用户有读写权限

Caddy 证书申请失败

# 查看 Caddy 日志
sudo journalctl -u caddy -n 50 --no-pager

常见原因:

  • DNS 未解析 — 域名必须已指向服务器公网 IP
  • 80/443 端口被占用 — 确保 Nginx/Apache 未占用这些端口
  • 防火墙未开放 — 需放行 80/tcp 和 443/tcp+udp

防火墙配置

# UFW
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp    # HTTP/3 QUIC
sudo ufw enable

修改 bootstrap 密码

RustBill 只在首次启动时(admin_users 表为空)使用 [bootstrap].admin_password 创建 admin 用户。如需重置:

  1. 登录 Admin 后台 → 用户管理 → 修改密码
  2. 或通过 CLI:rustbill-cli user passwd admin --new-password "xxx"

更新 RustBill

# 重新运行部署脚本(自动下载最新版本并替换二进制)
bash <(curl -fsSL https://raw.githubusercontent.com/zyxisme/rustbill-releases/main/deploy.sh)

# 重启服务
sudo systemctl restart rustbill-server

下一步

RustBill 运维手册

RustBill 的生产部署与运维参考手册。目标读者:生产环境中的系统管理员。


1. 系统架构概览

RustBill 是基于 Rust 的云服务器分销计费与开通平台,采用 Cargo workspace 多 crate 结构:

Crate职责
rustbill-protoprotobuf 定义 + tonic 代码生成(公共契约)
rustbill-core领域模型 + 全部 trait(插件/存储抽象)
rustbill-store-pgPostgreSQL 存储实现(sqlx)
rustbill-servergRPC 服务端(tonic),插件加载,Session+JWT 双认证,内嵌 Admin SPA
rustbill-cli最高权限 CLI 管理工具(直接 DB 访问),含 TUI 交互模式
rustbill-provider-*服务器资源供应商插件
rustbill-gateway-*支付网关插件
rustbill-notifier-*通知渠道插件

1.1 运行环境依赖

  • Rust 1.80+(编译);运行时仅需 glibc 2.35+(ubuntu-22.04 构建产物兼容绝大多数现代 Linux 发行版)
  • PostgreSQL 16+(数据存储,9 个迁移文件:001_initial009_plugins_table
  • Redis(可选,用于 session 缓存、分布式锁、跨实例速率限制;不可用时自动降级)
  • Node.js 22+ + npm(前端构建;运行时不需要)

1.2 服务端口与协议

端口协议说明
50051gRPC (HTTP/2)tonic 服务端主端口
80/443HTTP/HTTPSCaddy/Nginx 反向代理对外暴露(含 gRPC-Web 路径)
/healthHTTP GET健康检查端点(Circuit-breaker 短路后续所有中间件层,直接返回 200 OK)
/admin/*HTTP GET内嵌 Admin SPA(通过 Tower Layer 嵌入 tonic server,SPA fallback 到 index.html)

1.3 Tower 中间件栈

ConcurrencyLimitLayer(30) → HealthRouteLayer(/health 短路) → RateLimitLayer
→ AdminUiLayer → CorsLayer → SessionAuthLayer → JwtAuthLayer
→ ApiKeyAuthLayer → LogContextLayer → GrpcWebLayer → gRPC services

每个认证中间件只处理自己的认证类型,无凭证时直接放行(pass-through),不做 401 拦截。


2. 配置文件参考

配置文件 config.toml(复制自 config.example.toml),支持 config.local.toml 覆盖(Figment 层叠合并)。数组替换而非追加。

2.1 [server] — 服务器配置

字段类型默认值说明
hoststring"127.0.0.1"gRPC 服务监听地址。生产环境反向代理前置时保持 127.0.0.1
portinteger50051gRPC 服务监听端口
max_concurrencyinteger30入口并发上限,超过排队(FIFO backpressure)
notify_emailstring""系统通知接收邮箱(订单通知等)

2.2 [db] — 数据库配置

字段类型默认值说明
urlstring(必填)PostgreSQL 连接字符串。格式:postgres://user:pass@host:5432/rustbill
max_connectionsinteger20连接池最大连接数
min_connectionsinteger2连接池最小连接数(启动时预建立)
idle_timeout_secsinteger300空闲连接超时秒数
max_lifetime_secsinteger1800连接最大生命秒数(到期后回收重建)
acquire_timeout_secsinteger5获取连接超时秒数
test_before_acquirebooleantrue取连接前执行 SELECT 1 剔除死连接

2.3 [jwt] — JWT 配置(Customer 认证)

字段类型默认值说明
secretstring(必填)JWT 签名密钥。必须 >= 32 字符。为空或为 "change-me-in-production" 时启动拒绝
expiry_hoursinteger24access_token 过期小时数

2.4 [session] — Session 配置(Admin 认证)

字段类型默认值说明
cookie_namestring"rustbill_session"httpOnly Session Cookie 名称
idle_timeout_minutesinteger1440空闲超时分钟数(24 小时)
absolute_timeout_hoursinteger168绝对超时小时数(7 天),无论是否活跃
cookie_securebooleanfalseCookie Secure 标志(生产环境经 HTTPS 代理时设为 true
cookie_same_sitestring"Lax"Cookie SameSite 策略(Strict / Lax / None

2.5 [bootstrap] — 初始管理员配置

字段类型默认值说明
admin_usernamestring"admin"初始管理员用户名
admin_passwordstring(必填)初始管理员密码(bcrypt 哈希存储,无默认值)

2.6 [rate_limit] — 速率限制配置

字段类型默认值说明
enabledbooleantrue是否启用速率限制
login_per_secinteger5每秒登录请求上限
register_per_secinteger1每秒注册请求上限

2.7 [redis] — Redis 配置(可选)

字段类型默认值说明
enabledbooleanfalse是否启用 Redis。启用后提供 Session 缓存 + 分布式锁 + 跨实例速率限制
urlstring"redis://127.0.0.1:6379"Redis 连接 URL
max_connectionsinteger10Redis 连接池大小
prefixstring"rustbill"Redis key 前缀(避免多实例/多环境 key 冲突)

2.8 [plugins] — 插件配置

字段类型默认值说明
plugins_dirstring"./plugins"Rune 脚本插件目录。PluginScanner 扫描此目录下 *.rn 文件并同步到数据库

3. 构建与部署

3.1 从源码构建

# 克隆仓库
git clone https://github.com/zyxisme/rustbill.git
cd rustbill

# 编译服务端(Release,二进制约 15-25MB stripped)
cargo build --release -p rustbill-server

# 编译 CLI 管理工具
cargo build --release -p rustbill-cli

# 跨平台编译(ARM64)
cargo build --release --target aarch64-unknown-linux-gnu

3.2 构建前端

# Admin SPA(内嵌于 rustbill-server)
cd web-admin
npm install
npm run build
# 产物:dist/ → 复制到 rustbill-server/admin-dist/

# Consumer SPA(独立部署)
cd web-consumer
npm install
npm run build
# 产物:dist/ → 部署到 Web 服务器(Caddy/Nginx/Apache)

3.3 Consumer 前端配置

Consumer 运行时配置位于 web-consumer/public/config.json

{
  "grpcEndpoint": "https://your-domain.com",
  "appTitle": "RustBill",
  "adminUrl": "https://admin.your-domain.com"
}
字段说明
grpcEndpointgRPC 服务完整 origin(跨域部署时必填,同源可省略)
appTitle页面标题(浏览器标签页显示)
adminUrl管理后台外部链接(前台 Dashboard 点击跳转)

品牌配置:web-consumer/brand.yaml(品牌名/Logo/强调色/导航/集群节点),npm run buildvite-plugin-brand.ts 将颜色和导航编译进 JS bundle。

3.4 PostgreSQL 准备

# 创建数据库
sudo -u postgres createdb rustbill

# 创建用户
sudo -u postgres psql -c "CREATE USER rustbill WITH PASSWORD 'your-password';"
sudo -u postgres psql -c "GRANT ALL ON DATABASE rustbill TO rustbill;"

# 赋予 schema 权限(迁移由应用启动时自动执行)
sudo -u postgres psql -d rustbill -c "GRANT ALL ON SCHEMA public TO rustbill;"

迁移由应用启动时 leader 实例自动执行,无需手动 sqlx migrate run。单实例部署时该实例即为 leader。

3.5 systemd Service

# /etc/systemd/system/rustbill.service
[Unit]
Description=RustBill gRPC Server
After=network.target postgresql.service redis.service
Wants=postgresql.service

[Service]
Type=simple
User=rustbill
Group=rustbill
WorkingDirectory=/opt/rustbill
ExecStart=/opt/rustbill/rustbill-server
Restart=on-failure
RestartSec=5

# 安全加固
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/opt/rustbill /var/log/rustbill
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes

# 资源限制
MemoryHigh=512M
MemoryMax=1G
CPUQuota=200%

# 日志
StandardOutput=journal
StandardError=journal
SyslogIdentifier=rustbill

# 环境变量
Environment="RUST_LOG=info"
Environment="RUST_LOG_FORMAT=json"

[Install]
WantedBy=multi-user.target
# 启用并启动
sudo systemctl daemon-reload
sudo systemctl enable rustbill
sudo systemctl start rustbill
sudo systemctl status rustbill

3.6 日志轮转

# /etc/logrotate.d/rustbill
/var/log/rustbill/*.log {
    daily
    rotate 30
    compress
    delaycompress
    missingok
    notifempty
    copytruncate
    dateext
    dateformat -%Y%m%d
}

如果使用 systemd journal(StandardOutput=journal),日志由 journald 管理,无需额外 logrotate 配置。

3.7 反向代理 (Caddy)

# /etc/caddy/Caddyfile
your-domain.com {
    # gRPC-Web
    handle /rustbill.* {
        reverse_proxy 127.0.0.1:50051 {
            transport http {
                versions h2c
            }
        }
    }

    # Admin 管理后台(内嵌于 rustbill-server)
    handle /admin* {
        reverse_proxy 127.0.0.1:50051
    }

    # Consumer 前台(独立部署的静态文件)
    handle {
        root * /var/www/rustbill-consumer
        file_server
        try_files {path} /index.html
    }
}

3.8 Docker Compose

# docker-compose.yml
version: '3.8'
services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: rustbill
      POSTGRES_USER: rustbill
      POSTGRES_PASSWORD: change-me
    volumes:
      - pgdata:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    volumes:
      - redisdata:/data
    ports:
      - "6379:6379"
    restart: unless-stopped

  rustbill:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "50051:50051"
    volumes:
      - ./config.toml:/opt/rustbill/config.toml:ro
      - ./config.local.toml:/opt/rustbill/config.local.toml:ro
      - ./plugins:/opt/rustbill/plugins
    depends_on:
      - postgres
      - redis
    restart: unless-stopped

volumes:
  pgdata:
  redisdata:

Dockerfile(多阶段构建):

# 构建阶段
FROM rust:1.80-bookworm AS builder
WORKDIR /app
COPY . .
RUN cargo build --release -p rustbill-server

# 运行阶段
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates libssl3 && rm -rf /var/lib/apt/lists/*
WORKDIR /opt/rustbill
COPY --from=builder /app/target/release/rustbill-server .
COPY config.toml .
COPY plugins/ plugins/
EXPOSE 50051
CMD ["./rustbill-server"]

4. 数据库管理

4.1 迁移状态

# CLI 查看迁移状态
cargo run -p rustbill-cli -- db status

# 手动 psql 查看
psql -d rustbill -c "SELECT version, description, installed_on FROM _sqlx_migrations ORDER BY version;"

当前迁移清单(9 个):

迁移文件说明
001_initial.sql初始基线(所有核心表、FK、CHECK 约束、索引)
002_operation_logs.sql操作日志表
003_config_schema.sqlplugin_interfaces 新增 config_schema 列
004_citus_prepare.sqlCitus 兼容准备(customer_id NOT NULL)
005_api_keys.sqlAPI Key 表
006_customer_api_key_flag.sqlcustomer 表 can_create_api_keys 标志
007_instance_detail_sections.sql实例详情自定义段
008_scriptable_plugins.sql脚本化插件系统迁移
009_plugins_table.sql独立 plugins 定义表

4.2 备份

# 完整备份(pg_dump 自定义格式)
pg_dump -Fc -d rustbill -f /backup/rustbill_$(date +%Y%m%d_%H%M%S).dump

# crontab 自动化每日备份(凌晨 2:00)
# crontab -e
0 2 * * * pg_dump -Fc -d rustbill -f /backup/rustbill_$(date +\%Y\%m\%d).dump && find /backup -name 'rustbill_*.dump' -mtime +30 -delete

4.3 恢复

# 停止服务后再恢复
sudo systemctl stop rustbill

# 恢复
pg_restore --clean --if-exists -d rustbill /backup/rustbill_20260526.dump

# 重新启动
sudo systemctl start rustbill

4.4 重建数据库

# 破坏式重建(开发/测试环境)
dropdb rustbill
createdb rustbill
# 启动服务后自动运行迁移

5. CLI 与 TUI 管理

5.1 CLI 命令

# 查看帮助
cargo run -p rustbill-cli -- --help

# 用户管理
cargo run -p rustbill-cli -- user list
cargo run -p rustbill-cli -- user create --username admin2 --password pass123 --role admin

# 插件列表
cargo run -p rustbill-cli -- plugin list

# 数据库状态
cargo run -p rustbill-cli -- db status

5.2 TUI 交互模式

cargo run -p rustbill-cli -- tui

8 个标签页:Dashboard / Admin Users / Cust Users / Customers / Products / Plugins / Instances / DB Status

快捷键功能
q / Esc退出
Tab / Shift+Tab切换标签页
1-8直接跳转到对应标签页
↑↓ / jk列表导航
Enter查看详情
r刷新数据
d删除/终止(确认弹窗)
e / x启用/禁用插件
y / n确认/取消弹窗
Backspace关闭详情面板

TUI 完全复用服务端数据库连接,零额外连接。安全退出保证终端恢复(TerminalGuard Drop guard)。


6. 日常运维操作

所有操作均可通过 Admin UI 或 CLI 完成。以下为 CLI 常用命令及对应 Admin UI 页面。

6.1 商品管理 (Products)

Admin UI: 商品管理 → Products
  • 创建商品:指定名称、描述、所属分组、动态规格(JSONB)、计费周期(monthly/quarterly/yearly)、价格
  • 编辑/删除商品
  • 从上游同步商品(Upstream 页 → 上游商品标签 → 勾选 → ImportUpstreamProducts)
  • 批量定价(Upstream 页 → 定价管理标签 → 按加价比例批量更新售价:cost_price × markup_ratio
  • 商品规格(specs JSONB)支持动态键值对,包括 cpu_cores / memory_gb / disk_gb / bandwidth_mbps / region / os 等

6.2 客户管理 (Customers)

Admin UI: 客户管理 → Customers
  • 创建客户:姓名/联系人/邮箱/电话/信用额度
  • 编辑客户信息
  • 余额管理:充值/扣款(MyBalance 页面可查看交易流水)
  • 启用 API Key 权限(can_create_api_keys 标志)

6.3 订单管理 (Orders)

Admin UI: 交易管理 → Orders

订单状态流转:

pending → paid → provisioning → active
                ├→ cancelled(开通失败退款)
                ├→ suspended
                ├→ refunded
                └→ completed
  • 创建订单:选择客户 + 商品 + 支付网关 + 计费周期
  • 支付订单(余额扣减)
  • 开通实例(自动由 event_worker 处理异步开通)
  • 取消/退款
  • 支付超时(24 小时自动取消)

6.4 实例管理 (Instances)

Admin UI: 基础设施 → Instances

实例状态:provisioning / running / stopped / suspended / terminated / error

  • 启动/停止/重启/终止实例
  • 查看实例详情(含插件自定义 section/iframe)
  • 终止是最终操作,不可恢复

6.5 支付管理 (Payments)

Admin UI: 交易管理 → Payments
  • 查看支付记录
  • 银行转账人工确认
  • 退款(全额/部分)
  • 支付回调幂等性(SELECT FOR UPDATE 原子化)

6.6 工单管理 (Tickets)

Admin UI: 系统管理 → Tickets

工单状态:pendingprocessingresolvedclosed

  • 创建/分配工单
  • 回复(含内部备注 is_internal=true,对客户不可见)
  • 分配处理人变更时自动通知新旧处理人

6.7 发票管理 (Invoices)

Admin UI: 交易管理 → Billing

发票状态:draftissuedpaid / overdue / cancelled

  • 手动创建发票
  • 批量生成(按客户+周期)
  • 标记为已付款

6.8 插件管理 (Plugins)

Admin UI: 系统管理 → Plugins

插件列表页分为两部分:

  1. 已安装插件.rn 定义按 type+id 去重)- 只读列表
  2. 接口实例 — 可创建/启用/禁用/编辑配置 JSON/执行健康检查

当前已实现的 7 个插件:

类型插件 ID说明
Notifiernotifier-webhookWebhook HTTP POST 通知
Notifiernotifier-emailSMTP 邮件通知
Gatewaygateway-banktransfer银行转账(本地逻辑)
Gatewaygateway-yipay易支付聚合支付(HTTP + MD5 签名)
UpstreamProviderprovider-rustbillRustBill 上游分销(gRPC-Web)
FirstPartyProviderprovider-incusIncus 虚拟化(mTLS REST API)

插件脚本存储在数据库 plugin_interfaces.script_source 列中,通过 Admin UI 编辑。修改后由 ScriptEngine.evict() 自动热重载。


7. 日志与监控

7.1 日志格式

RustBill 使用 tracing-subscriber

# 开发环境 — 彩色文本
RUST_LOG=rustbill_server=debug cargo run -p rustbill-server

# 生产环境 — JSON 格式(日志采集)
RUST_LOG=info RUST_LOG_FORMAT=json cargo run -p rustbill-server

# 特定模块调试
RUST_LOG=rustbill_server::plugin_scanner=debug,rustbill_server::services=info

7.2 健康检查

# 基本健康检查
curl -s http://localhost:50051/health
# 预期响应: 200 OK(空 body)

# 就绪检查(含 DB 连接池验证)
curl -s http://localhost:50051/ready
# 200 就绪,503 未就绪

7.3 关键日志事件

事件日志级别说明
服务启动INFO实例身份注册、leader 选举结果、插件扫描完成
Leader 选举INFO当选/续期/释放 leader 角色
数据库迁移INFO迁移执行状态
插件扫描INFO / WARN新增/更新插件定义,文件解析异常
gRPC 请求DEBUG请求路径、耗时、状态码
认证失败WARNsession/JWT/API key 验证失败
支付回调INFO支付网关回调处理
事件消费DEBUGevent_worker 处理订单事件
熔断器WARNCircuitBreaker 开/半开/关状态变更
Redis 降级WARNRedis 不可用时自动降级通知
实例心跳TRACE每 10s 心跳更新(生产环境建议不输出)

7.4 metrics 端点

# OpenTelemetry OTLP 导出(需要配置 OTLP exporter endpoint)
# 导出指标:gRPC 请求计数/延迟、DB 连接池状态、熔断器状态、事件队列深度

8. 升级流程

8.1 标准升级(单实例)

# 1. 拉取最新代码
cd /opt/rustbill
git pull origin main

# 2. 编译新版本
cargo build --release -p rustbill-server

# 3. 构建前端(如有变更)
cd web-admin && npm install && npm run build
cp -r dist ../rustbill-server/admin-dist/

# 4. 停止服务
sudo systemctl stop rustbill

# 5. 替换二进制
cp target/release/rustbill-server /opt/rustbill/

# 6. 启动服务
sudo systemctl start rustbill

# 7. 验证
sudo systemctl status rustbill
curl -s http://localhost:50051/health

8.2 零停机升级(多实例 + 反向代理)

# 前提:多实例部署,反向代理轮询
# 1. 逐个从负载均衡摘除实例 → 升级 → 加回
# 2. Leader 选举自动切换:关闭实例的 leader role 自动过期
# 3. 最后验证所有实例正常

8.3 数据库迁移

迁移在 leader 实例启动时自动运行。如果迁移失败,leader 持有 migration 角色锁阻止其他实例启动。

手动查看迁移状态:

psql -d rustbill -c "SELECT version, description, installed_on, success FROM _sqlx_migrations ORDER BY version;"

9. 故障排查

9.1 数据库连接失败

错误: PoolTimedOut / connection refused
  1. 检查 PostgreSQL 是否运行:sudo systemctl status postgresql
  2. 验证 pg_hba.conf 允许应用用户连接:sudo -u postgres psql -c "SHOW hba_file;"
  3. 使用 config.toml 中的连接参数测试:psql "postgres://rustbill:password@localhost:5432/rustbill" -c "SELECT 1;"
  4. 检查防火墙规则:sudo ufw status
  5. 连接池耗尽:增大 [db].max_connections 或缩短 idle_timeout_secs

9.2 端口被占用

# 查找占用端口的进程
sudo lsof -i :50051
# 或
sudo ss -tlnp | grep 50051

# 停止占用进程
sudo kill -TERM <PID>

9.3 systemd 启动失败

# 查看最近日志
sudo journalctl -u rustbill -n 50 --no-pager

# 持续跟踪
sudo journalctl -u rustbill -f

# 常见原因
# - 配置文件缺失或格式错误(config.toml)
# - JWT secret 未设置或过短(< 32 字符)
# - bootstrap admin_password 未设置
# - 数据库 URL 错误或数据库不存在
# - 端口已被占用

9.4 Admin 管理后台白屏

  1. 硬刷新浏览器(Ctrl+Shift+R
  2. 检查 admin-dist/index.html 是否存在:ls /opt/rustbill/admin-dist/
  3. 检查 Nginx/Caddy 配置:/admin* 路径是否正确代理到 localhost:50051
  4. 检查浏览器控制台是否有 JS 资源 404 错误
  5. 重新构建并部署 admin-dist:cd web-admin && npm run build && cp -r dist /opt/rustbill/rustbill-server/admin-dist/

9.5 插件无法加载

  1. 检查 Admin UI → Plugins 页面中对应接口实例的 enabled 状态
  2. 检查 plugins/ 目录下 .rn 文件是否存在
  3. 检查 plugin_interfaces 表的 script_source 列是否为空
  4. 检查服务器日志中 PluginScanner 的 WARN/ERROR 日志
  5. Rune 脚本语法错误:脚本编译失败但服务器仍可正常运行,错误仅影响对应插件

9.6 HTTPS 证书问题

# Caddy 自动管理证书,确认:
# 1. DNS 解析正确:dig your-domain.com
# 2. 防火墙开放 80/443 端口:sudo ufw allow 80/tcp && sudo ufw allow 443/tcp
# 3. Caddy 日志:sudo journalctl -u caddy -f

9.7 内存使用过高

# 查看当前内存使用
sudo systemctl show rustbill -p MemoryCurrent

# 调整 systemd 资源限制
sudo systemctl edit rustbill

# 添加或修改:
# [Service]
# MemoryHigh=768M
# MemoryMax=1.5G

sudo systemctl daemon-reload
sudo systemctl restart rustbill

9.8 gRPC 调用超时

# 1. 检查服务是否可达
grpcurl -plaintext localhost:50051 list

# 2. 检查请求是否进入(日志级别调整为 debug)
RUST_LOG=rustbill_server=debug cargo run -p rustbill-server

# 3. 检查 ConcurrencyLimit 是否已达上限(默认 30)
# 增大 max_concurrency:在 config.toml [server] 节设置 max_concurrency = 100

# 4. 检查数据库连接池是否耗尽
# 增大 [db].max_connections 或检查慢查询

9.9 gRPC-Web trailers-only 错误

浏览器收到 200 OK 但 grpc-status header 非 0。这是 gRPC 错误的标准传递方式。

  1. 检查 grpc-status header 值(gRPC 状态码)
  2. 检查 grpc-message header 值(错误描述)
  3. 常见原因:认证失败(session 过期/JWT 过期)、参数校验失败、权限不足

9.10 Redis 故障降级

当 Redis 不可用时,系统自动降级:

功能Redis 正常Redis 降级
Session 缓存Redis cache-aside纯 DB 查询
速率限制跨实例共享计数单实例内存计数
分布式锁Redis SET NXPG advisory lock

降级时系统正常运行但跨实例协调能力下降(速率限制不跨实例、锁竞争通过 PG 完成)。日志中会出现 WARN 级别的 Redis 连接失败信息。


10. 环境变量参考

变量说明示例
RUST_LOGtracing 日志级别过滤器info / rustbill_server=debug / warn
RUST_LOG_FORMAT日志输出格式不设置=彩色文本 / json=JSON 格式
DATABASE_URLPostgreSQL 连接(覆盖 config.toml)postgres://user:pass@host/db
CONFIG_FILE主配置文件路径/etc/rustbill/config.toml
LOCAL_CONFIG_FILE本地覆盖配置路径/etc/rustbill/config.local.toml
PROTOCprotoc 二进制路径(编译时需要)/usr/local/bin/protoc
CARGO_BUILD_JOBSCargo 并行编译任务数1(低内存环境限制)

11. 安全检查清单

  • config.toml[jwt].secret 已设置为 >= 32 字符的随机字符串(非 "change-me-in-production"
  • [bootstrap].admin_password 已设置为强密码
  • 生产环境 [session].cookie_secure 已设为 true(经 HTTPS 代理)
  • 生产环境 CorsLayer 已配置 allowed_origins 白名单(非 permissive)
  • 防火墙仅开放必要端口(80/443,禁止 50051 直接暴露公网)
  • systemd 安全加固已启用(NoNewPrivileges=yesProtectSystem=strict
  • 数据库用户密码不为默认值
  • config.tomlconfig.local.toml 文件权限为 600(仅 owner 可读写)
  • 备份 cron 任务已配置并验证
  • .claude/ 目录在 .gitignore 中(防止 Agent worktree 被提交)

12. 常见部署拓扑

12.1 最小部署(单机)

Client → Caddy (:443) → rustbill-server (:50051) → PostgreSQL (:5432)
                         ↓ (Admin SPA 内嵌)
                         ↓ (Consumer SPA 静态文件由 Caddy 直接 serve)

12.2 带 Redis 的中型部署

Client → Caddy (:443) → rustbill-server (:50051) → PostgreSQL (:5432)
                                                   → Redis (:6379)

12.3 多实例高可用部署

                    ┌→ rustbill-server-1 (:50051) ──┐
Client → Caddy LB ─┼→ rustbill-server-2 (:50051) ──┼→ PostgreSQL (Citus 多 coordinator)
                    └→ rustbill-server-3 (:50051) ──┘→ Redis (实例间不共享)

多实例模式下:

  • Leader 选举通过 PG leader_role 表锁进行(迁移/事件消费/计费各独立角色)
  • 支付回调通过 SELECT FOR UPDATE 原子化防重
  • 事件队列通过 FOR UPDATE SKIP LOCKED 并发消费
  • 熔断器按 host 独立维护
  • 速率限制在 Redis 启用时跨实例共享,降级后单实例独立计数

13. 插件脚本热更新

编辑插件脚本后无需重启服务:

  1. Admin UI → Plugins → 接口实例 → 编辑脚本源码
  2. 保存到数据库 plugin_interfaces.script_source
  3. ScriptEngine.evict(key) 自动清除缓存
  4. 下次调用该插件时自动重新编译并执行新版本
# 也可以手动替换 plugins/*.rn 文件
# PluginScanner 每 5 分钟后台任务检测文件变化
# 文件系统版本 > DB 版本时自动更新 plugins 表

14. 金额精度说明

所有金额使用 DECIMAL(12,2) 数据库类型,Rust 端以 rust_decimal::Decimal 承载,序列化始终保留 2 位小数。

涉及金额的 6 张表:

金额列
productsprice
orderstotal_amount
invoicesamountpaid_amount
paymentsamount
customersbalancecredit_limit
balance_transactionsamountbalance_beforebalance_after

高可用与分布式部署指南

RustBill 从设计之初就面向水平扩展和高可用部署。本文档涵盖从单机到 Citus 分布式集群的完整演进路径。


架构总览

                      DNS Round-Robin / VIP
                             │
          ┌──────────────────┼──────────────────┐
          │                  │                  │
      Caddy-1            Caddy-2           Caddy-N
   (HTTP/3+TLS)       (HTTP/3+TLS)       (HTTP/3+TLS)
          │                  │                  │
          └──────┬───────────┴──────────┬───────┘
                 │   health-check       │
                 │   least_conn LB      │
                 │                      │
          ┌──────┴──────────────────────┴───────┐
          │         rustbill-server × N         │
          │   (stateless, tonic gRPC :50051)    │
          └──────┬──────────────────────┬───────┘
                 │                      │
          ┌──────┴──────┐        ┌──────┴──────┐
          │    Redis    │        │    OTLP     │
          │ (optional)  │        │  Collector  │
          └─────────────┘        └─────────────┘
                 │
          ┌──────┴──────────────────────────────┐
          │          Citus Coordinator          │
          │   (PG protocol routing, metadata)   │
          └──────┬───────────────────┬──────────┘
                 │                   │
          ┌──────┴──────┐     ┌──────┴──────┐
          │  Worker-1   │     │  Worker-2   │
          │(shard 1-32) │     │(shard 33-64)│
          │  + replica  │     │  + replica  │
          └─────────────┘     └─────────────┘
                 │                   │
          ┌──────┴───────────────────┴──────┐
          │        etcd cluster × 3         │
          │      (Patroni distributed       │
          │     consensus for failover)     │
          └─────────────────────────────────┘
### 核心设计原则

| 原则 | 实现方式 |
|------|---------|
| **无状态应用层** | rustbill-server 不存本地状态,Session 入 Redis/DB |
| **PG 做共识** | 领导选举和事件外发箱全部基于 PostgreSQL 实现,无额外依赖 |
| **优雅降级** | Redis 不可用时自动回退到 PG;Rate Limit DB 模式 |
| **故障自愈** | 熔断器自动切断故障下游,恢复后半开探测 |
| **可观测性** | Request ID 全链路追踪 + OTLP 导出 + 健康检查端点 |

---

## 1. 多实例水平扩展

### 1.1 应用层无状态

rustbill-server 每个实例完全无状态:

- **Session** — Redis cache-aside 优先,miss 回源 `sessions` 表。Redis 不可用时直接查 DB。
- **JWT** — 自包含 token,任意实例可独立验证。
- **插件** — Rune 脚本源码存储于 DB `plugin_interfaces.script_source`。`ScriptEngine` 内存缓存,热重载自动同步。
- **后台任务** — 通过领导选举确保单实例执行(见第 3 节)。

所有实例共享同一份 `config.toml`,指向同一个 DB 和 Redis。无状态使得扩缩容只需增减 Caddy upstream 列表。

### 1.2 Caddy 反向代理

```caddyfile
# /etc/caddy/Caddyfile
rustbill.example.com {
    # HTTP/3 QUIC
    protocols h1 h2 h3

    # 上游 gRPC 服务
    reverse_proxy /rustbill.* {
        to localhost:50051 localhost:50052
        health_uri /health
        health_interval 5s
        lb_policy least_conn

        # gRPC 需要 HTTP/2 传输
        transport http {
            versions h2c
        }
    }

    # Admin 静态资源(内嵌于 server)
    reverse_proxy /admin* {
        to localhost:50051 localhost:50052
        lb_policy least_conn
    }

    # 日志
    log {
        output file /var/log/caddy/rustbill.log
        format json
    }
}

关键配置项:

配置推荐值说明
health_uri/health短路端点,仅返回 200,不做任何 DB 查询
health_interval5s健康检查频率。过短增加负载,过长延迟故障检测
lb_policyleast_conn最少连接数算法,适合 gRPC 长连接场景
transport http versionsh2cgRPC 要求 HTTP/2 传输层

1.3 并发控制

[server]
max_concurrency = 30   # 每实例并发上限

每个 rustbill-server 实例在 Tower 中间件栈入口处设置 ConcurrencyLimitLayer。超过上限的请求进入 FIFO 队列等待(背压),而非直接拒绝。集群总并发容量 = N × max_concurrency

1.4 速率限制

[rate_limit]
enabled = true
login_per_sec = 5       # 登录 5 次/秒/IP
register_per_sec = 1    # 注册 1 次/秒/IP

双后端模式:

后端配置适用场景
内存[redis] enabled = false单实例或允许独立计数
Redis[redis] enabled = true多实例共享计数,全局精确限流

Redis 不可用时自动降级到内存(per-instance 计数,非精确)。


2. 实例标识与心跳

2.1 InstanceIdentity

#![allow(unused)]
fn main() {
// 启动时生成 UUID v7(时间有序)
pub struct InstanceIdentity {
    pub id: Uuid,  // UUID v7
}
}

每个 rustbill-server 实例启动时生成一个 UUID v7,注册到 instance_heartbeat 表:

CREATE TABLE instance_heartbeat (
    instance_id   UUID PRIMARY KEY,
    registered_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    heartbeat_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    pool_idle     INTEGER NOT NULL DEFAULT 0,   -- 空闲连接数
    pool_active   INTEGER NOT NULL DEFAULT 0,   -- 活跃连接数
    pool_max      INTEGER NOT NULL DEFAULT 0    -- 最大连接数
);

UUID v7 采用时间有序编码,便于日志排序和调试。

2.2 心跳机制

每 10 秒更新 heartbeat_at + 连接池快照(pool_idle / pool_active):

启动 → InstanceIdentity::new(注册)
  → spawn InstanceHeartbeat (interval 10s)
  → 更新 heartbeat_at + pool_{idle,active}

心跳数据被领导选举用于负载感知调度(见第 3 节)。心跳超时 30 秒的实例被视为失联,其持有的 leader 角色自动释放。


3. 领导选举

3.1 基于 PostgreSQL 表锁

RustBill 不引入 ZooKeeper/etcd 等外部共识组件,直接利用 PostgreSQL 的行锁实现领导选举。

#![allow(unused)]
fn main() {
pub struct LeaderElection {
    pool: PgPool,
    instance_id: Uuid,
    held: Arc<RwLock<HashSet<String>>>,  // 当前持有的角色
}
}

选举 SQL(try_acquire):

UPDATE leader_role
SET holder = $instance_id, heartbeat_at = NOW(), ttl_secs = 30
WHERE role = $role
  AND heartbeat_at < NOW() - make_interval(secs => COALESCE(
      (SELECT ttl_secs FROM leader_role WHERE role = $role), 30))
  AND holder IN (
    SELECT instance_id FROM instance_heartbeat
    WHERE heartbeat_at > NOW() - INTERVAL '30 seconds'
    ORDER BY pool_idle DESC, instance_id ASC LIMIT 1
  )

关键机制:

  • FOR UPDATE 隐式行锁UPDATE 在 PG 中自动获取行级排他锁,多实例并发执行时只有一个成功。
  • 心跳过期 — TTL 30 秒。leader 不续期时租约自动过期,其他实例可抢占。
  • 存活检查 — 仅心跳正常的实例(instance_heartbeat.heartbeat_at > NOW() - 30s)可参与选举。
  • 负载感知ORDER BY pool_idle DESC, instance_id ASC 优先选举空闲连接最多的实例。

3.2 三种 Leader 角色

角色职责由谁持有
migration数据库迁移执行启动时最先选举,仅 leader 执行 sqlx::migrate!
billing定时账单生成(月末批量)仅 leader 运行 billing cron job
event_worker事件队列消费、支付超时扫描仅 leader 运行 event worker loop

三种角色独立选举,可分布在不同实例上,避免单点瓶颈。

3.3 续期与释放

#![allow(unused)]
fn main() {
// 每 10 秒续期,保持租约活跃
leader_election.spawn_renewal("event_worker".to_string(), 10);
// 实例关闭时自动释放
impl Drop for LeaderElection {
    // release all held roles
}
}

续期后台任务每 10 秒更新 heartbeat_at = NOW()。若 leader 崩溃,TTL 30 秒后自动释放,follower 接管。

3.4 启动流程

create_pool → InstanceIdentity::new(注册) → LeaderElection(migration)
  ├── leader:   run_migrations + spawn_renewal("migration")
  └── follower: poll leader_role 等待 leader(最多 60s)
→ spawn InstanceHeartbeat(10s)
→ 正常启动 gRPC server
→ spawn PluginScanner
→ spawn event_worker(竞争 leader 后才开始消费)
→ spawn 其他后台任务

Follower 在迁移完成前阻塞,确保不会在旧 schema 上运行。


4. 事务外发箱与事件 Worker

4.1 事件队列表

CREATE TABLE event_queue (
    id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    event_type  TEXT NOT NULL,          -- order_paid / order_provisioned / order_cancelled
    payload     JSONB NOT NULL,
    status      TEXT NOT NULL DEFAULT 'pending',  -- pending/processing/done/failed
    order_id    UUID NOT NULL,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

业务操作在同一数据库事务中写入业务数据 + event_queue 记录,保证原子性。典型流程:

BEGIN;
  -- 余额扣减
  UPDATE customers SET balance = balance - $amount;
  -- 创建支付记录
  INSERT INTO payments ...;
  -- 更新订单状态
  UPDATE orders SET status = 'provisioning';
  -- 插入事件
  INSERT INTO event_queue (event_type, payload, order_id)
    VALUES ('order_paid', $payload, $order_id);
COMMIT;

4.2 Worker 消费

#![allow(unused)]
fn main() {
// 每 5 秒轮询
loop {
    // 1. 检查是否仍是 event_worker leader
    if !leader_election.is_leader("event_worker").await { break; }

    // 2. 获取 pending 事件(并发安全)
    let events = sqlx::query(
        "SELECT id, event_type, payload, order_id
         FROM event_queue
         WHERE status = 'pending'
         ORDER BY created_at ASC
         LIMIT 10
         FOR UPDATE SKIP LOCKED"
    ).fetch_all(&pool).await?;

    // 3. 按 order_id 获取 advisory lock → 串行化同订单事件
    for event in events {
        let key = uuid_to_advisory_key(&event.order_id);
        sqlx::query("SELECT pg_try_advisory_xact_lock($1)")
            .bind(key).fetch_one(&pool).await?;
        // dispatch: provision → notify
    }
}
}

并发安全保证:

  • FOR UPDATE SKIP LOCKED — 多个 worker 不会重复消费同一事件
  • pg_try_advisory_xact_lock(hash(order_id)) — 同订单事件串行执行,跨订单并发执行
  • 事务释放时自动释放 advisory lock,无需显式解锁

4.3 事件类型与处理

事件触发时机处理逻辑成功后
order_paid支付完成调用 provider 开通实例写入 order_provisioned 事件
order_provisioned开通成功广播通知(客户 + 管理员)标记 done
order_cancelled订单取消退款 + 广播通知标记 done

开通失败时自动退款 + 写入 order_cancelled 事件触发通知(Saga 补偿)。

4.4 支付超时扫描

Worker 同时扫描 24 小时未支付的订单:

UPDATE orders SET status = 'cancelled'
WHERE status = 'pending'
  AND created_at < NOW() - INTERVAL '24 hours'
RETURNING id

取消的订单同样写入 order_cancelled 事件,走退款+通知流程。


5. 熔断器

5.1 三段状态机

  ┌──────────┐    5 次失败     ┌──────────┐
  │  Closed   │ ──────────────→ │   Open    │
  │ (正常)    │                 │ (熔断)    │
  └──────────┘                 └─────┬─────┘
       ↑                              │
       │          30s 冷却后           │
       │    ┌──────────────────┐      │
       └────┤    HalfOpen      │←─────┘
            │ (探测)           │
            └────┬─────────────┘
                 │
         成功 ───┴─── 失败 → Open
  • Closed — 正常状态,失败计数器递增。累计 5 次失败 → Open。
  • Open — 熔断状态,直接拒绝请求(抛出 CircuitOpen 错误),无需等待超时。
  • HalfOpen — 30 秒冷却后进入,允许 1 次探测请求。成功 → Closed;失败 → Open。

5.2 按 host 粒度的熔断器管理

#![allow(unused)]
fn main() {
pub struct BreakerManager {
    breakers: RwLock<HashMap<String, Arc<CircuitBreaker>>>,
}
}

熔断器按下游 host 隔离,一个支付网关故障不影响其他网关。用于所有出站 HTTP 调用:

  • 支付网关(易支付、自定义网关)
  • 上游供应商(RustBill 上游实例)
  • 通知渠道(Webhook)

5.3 CoreHttpClient

#![allow(unused)]
fn main() {
pub struct CoreHttpClient {
    client: reqwest::Client,
    cert_clients: RwLock<HashMap<String, reqwest::Client>>,  // mTLS client 缓存
    breakers: BreakerManager,
}
}

统一出站 HTTP 客户端,提供:

  • 3 次重试 — 指数退避,透明重试网络瞬时错误
  • 熔断检查 — 请求前检查 host 熔断器状态,Open 时快速失败
  • mTLS 证书缓存 — 按 cert hash 缓存 reqwest::Client 实例,避免每次重建 TLS 握手

6. Redis 缓存层(可选)

Redis 是可选组件。不配置时全部功能降级运行,零阻塞。

6.1 功能矩阵

功能Redis 可用Redis 不可用(降级)
Session 缓存cache-aside(快)纯 DB 查询(慢,但可用)
分布式锁Redis SETNXPG advisory lock
跨实例限流Redis 全局计数内存 per-instance 计数(近似)

6.2 配置

[redis]
enabled = true
url = "redis://localhost:6379"
max_connections = 10
prefix = "rustbill"

prefix 用于多环境隔离(如 rustbill-prod / rustbill-staging 共享同一 Redis)。

6.3 降级策略

Session 读取:
  Redis GET → hit → 返回
           → miss → DB 查询 → Redis SET (异步, 忽略失败) → 返回

Rate Limit:
  Redis INCR → 成功 → 全局精确
            → 失败 → 内存计数器 (per-instance)

分布式锁:
  Redis SETNX → 成功 → 获取锁
             → 失败 → PG pg_try_advisory_lock

所有 Redis 操作失败后自动降级,不阻塞业务。恢复后自动切回。


7. Citus 分布式数据库

当单 PostgreSQL 实例无法承载数据量或并发时,迁移到 Citus。

7.1 分片策略

                    ┌──────────────────┐
                    │   Coordinator     │
                    │  (metadata only)  │
                    └──┬────────────┬──┘
                       │            │
              ┌────────┴──┐   ┌────┴──────────┐
              │  Worker-1  │   │  Worker-2      │
              │  shard 1-32│   │  shard 33-64   │
              │  (PG 16)   │   │  (PG 16)       │
              │  + replica │   │  + replica     │
              └───────────┘   └───────────────┘

分片键:customer_id。按 customer_id 哈希分片,每个客户的所有关联数据落在同一 Worker。

7.2 分布式表(Distributed Tables)

customer_id 分片的表:

表名分片键说明
customerscustomer_id (id)客户主体
customer_userscustomer_id客户子账户
orderscustomer_id订单
invoicescustomer_id账单
invoice_itemscustomer_id账单明细
paymentscustomer_id支付记录
instancescustomer_id云实例
balance_transactionscustomer_id余额变动
ticketscustomer_id工单
ticket_repliescustomer_id工单回复

同客户数据共置:一个客户的所有订单、支付、实例、工单存于同一 Worker。跨 Worker Join 几乎不发生。

7.3 参考表(Reference Tables)

复制到所有 Worker 的表(全量同步,高频读取):

表名说明
admin_users管理员账户
sessionsSession 记录
products商品定义
product_categories商品分类
product_groups商品分组
plugin_interfaces插件接口实例
plugins插件定义
instance_heartbeat实例心跳
leader_role领导选举
event_queue事件队列
api_keysAPI 密钥

7.4 迁移 004 前置条件

004_citus_prepare.sql 确保所有涉及 customer_id 的列均为 NOT NULL,这是 Citus 创建分布式表的前提。此迁移与单节点 PG 完全兼容。

7.5 初始化集群

-- 在 Coordinator 上执行

-- 添加 Worker 节点
SELECT citus_add_node('worker-1.internal', 5432);
SELECT citus_add_node('worker-2.internal', 5432);

-- 创建分布式表(示例)
SELECT create_distributed_table('customers', 'id');
SELECT create_distributed_table('orders', 'customer_id');
SELECT create_distributed_table('payments', 'customer_id');
SELECT create_distributed_table('instances', 'customer_id');
-- ... 其余分布式表

-- 创建参考表
SELECT create_reference_table('admin_users');
SELECT create_reference_table('products');
SELECT create_reference_table('plugin_interfaces');
-- ... 其余参考表

推荐分片数:64(2 Worker 各 32,后续可水平扩容)。

7.6 在线扩容

-- 添加新 Worker
SELECT citus_add_node('worker-3.internal', 5432);

-- 开始数据重分布(零停机)
SELECT citus_rebalance_start();

-- 监控进度
SELECT * FROM citus_rebalance_status();

Citus 在后台迁移分片,读写正常进行。完成后分片自动重新分布到全部 Worker。


8. Worker 高可用 — Patroni + etcd

每个 Citus Worker 独立配置 PG 流复制 + Patroni 自动故障切换。

8.1 架构

              ┌─────────────────────────────────┐
              │          etcd cluster           │
              │ (3 nodes, distributed concensus)  │
              └──┬─────────────┬─────────────┬──┘
                 │              │              │
         ┌───────┴──────┐ ┌────┴──────┐ ┌────┴──────┐
         │ Worker-1      │ │ Worker-2   │ │ Worker-3   │
         │ ┌───────────┐ │ │            │ │            │
         │ │ Patroni   │ │ │ Patroni    │ │ Patroni    │
         │ │ (leader)  │ │ │ (replica)  │ │ (replica)  │
         │ └─────┬─────┘ │ │            │ │            │
         │       │       │ │            │ │            │
         │  ┌────┴────┐  │ │ ┌────────┐ │ │ ┌────────┐ │
         │  │ PG 16    │  │ │ │ PG 16  │ │ │ │ PG 16  │ │
         │  │ (master) │──┼─┼→│(standby)│ │ │ │(standby)│ │
         │  └─────────┘  │ │ └────────┘ │ │ └────────┘ │
         └───────────────┘ └────────────┘ └────────────┘
              streaming       streaming       streaming
              replication     replication     replication

8.2 Patroni 配置

# /etc/patroni/patroni.yml
scope: rustbill-worker1
name: worker1-node1

restapi:
  listen: 0.0.0.0:8008
  connect_address: worker1-node1.internal:8008

etcd:
  hosts: etcd1.internal:2379,etcd2.internal:2379,etcd3.internal:2379

bootstrap:
  dcs:
    ttl: 30
    loop_wait: 10
    retry_timeout: 10
    maximum_lag_on_failover: 1048576     # 1MB
    postgresql:
      use_pg_rewind: true
      parameters:
        max_connections: 200
        shared_buffers: 8GB
        wal_level: replica
        hot_standby: "on"
        max_wal_senders: 10
        wal_keep_size: 1GB

postgresql:
  listen: 0.0.0.0:5432
  connect_address: worker1-node1.internal:5432
  data_dir: /var/lib/postgresql/16/main
  pg_hba:
    - host replication replicator 0.0.0.0/0 md5
    - host all all 0.0.0.0/0 md5

8.3 故障切换流程

1. Patroni 检测 leader 心跳丢失(etcd TTL 30s)
2. 选举新 leader(etcd CAS 操作,只有一个 replica 成功)
3. 新 leader 提升为 master(pg_ctl promote)
4. 其他 replica 切换到新 master(rewind + follow)
5. Coordinator 自动感知(连接池重连到新 master VIP)

切换时间: 通常 30-60 秒。

8.4 etcd 集群

# 三节点 etcd 集群部署
# etcd1
etcd --name etcd1 \
  --initial-cluster etcd1=http://etcd1.internal:2380,etcd2=http://etcd2.internal:2380,etcd3=http://etcd3.internal:2380 \
  --initial-cluster-state new

# etcd2, etcd3 同理

etcd 集群容忍 1 节点故障(3 节点 majority = 2)。


9. 监控与可观测性

9.1 健康检查端点

端点用途行为
GET /healthCaddy 健康检查立即返回 200,短路所有中间件
GET /ready就绪检查检查 DB 连接池连通性
# Caddy 使用 /health
curl -s http://localhost:50051/health
# → 200 OK

# k8s readiness probe 使用 /ready
curl -s http://localhost:50051/ready
# → 200 OK 或 503(DB 不通)

9.2 请求追踪

每个请求携带:

  • X-Request-Id — UUID v7,由服务端生成
  • traceparent — W3C Trace Context 标准头,格式 00-{trace_id}-{span_id}-01
# 示例响应头
X-Request-Id: 018f6a3c-8b7e-7d01-9a2c-c3f1e4b8d5a2
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01

9.3 OpenTelemetry

[telemetry]
enabled = true
otlp_endpoint = "http://localhost:4317"
service_name = "rustbill"

导出到 OTLP Collector(Jaeger / Grafana Tempo),包含:

  • gRPC 请求 span(method、status code、duration)
  • DB 查询 span(sqlx 自动注入)
  • 出站 HTTP span(CoreHttpClient 自动注入)
  • 事件处理 span

9.4 systemd 健康检查定时器

# /etc/systemd/system/rustbill-healthcheck.service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/grpcurl -plaintext localhost:50051 rustbill.identity.IdentityService/GetMe
User=rustbill

# /etc/systemd/system/rustbill-healthcheck.timer
[Timer]
OnCalendar=*:0/5              # 每 5 分钟
Persistent=true

[Install]
WantedBy=timers.target

9.5 数据库监控查询

-- 活跃 Worker 节点
SELECT node_name, node_port, is_active, should_haveshards
FROM citus_get_active_worker_nodes();

-- 分片分布
SELECT shard_count, nodename, nodeport
FROM citus_shards
GROUP BY nodename, nodeport;

-- 租户数量(每个 shard 的行数)
SELECT shardid, shard_size, result::json->0->>'row_count' AS rows
FROM citus_shard_sizes()
JOIN LATERAL json_array_elements(shard_sizes::json) AS result ON true
WHERE table_name = 'customers'::regclass;

-- 跨分片查询监控(慢查询诊断)
SELECT queryid, query, calls, mean_exec_time
FROM citus_stat_statements
WHERE calls > 0
ORDER BY mean_exec_time DESC
LIMIT 10;

9.6 资源监控

# 服务状态(内存/CPU)
systemctl status rustbill-server

# 连接数
ss -tnp | grep :50051 | wc -l

# 数据库连接
psql -c "SELECT count(*) FROM pg_stat_activity WHERE datname = 'rustbill'"

10. 故障恢复

10.1 故障场景与响应

场景影响自动恢复手动操作
单个 server 实例宕机无(Caddy 自动摘除)Caddy health check 检测后摘除重启故障实例
所有 server 实例宕机服务完全中断逐台重启
Worker 宕机 + 有 replica无(Patroni 自动切换)30-60s 自动 failover修复故障节点后重新加入
Worker 宕机 + 无 replica该 Worker 的分片不可用从备份恢复
Coordinator 宕机所有查询中断无(需手动切换)DNS/VIP 切换到备用 Coordinator
Redis 宕机性能下降(无中断)自动降级到 PG恢复 Redis
etcd 单节点宕机无(集群仍可工作)集群自愈恢复故障节点
etcd 多节点宕机Patroni 无法选举恢复 etcd 集群
下游 API 熔断该下游请求快速失败30s 后半开探测,成功后恢复修复下游后等待自动恢复

10.2 Worker 故障恢复步骤

# 1. 确认 Worker 状态
psql -h coordinator -c "SELECT * FROM citus_get_active_worker_nodes();"

# 2. 如果 Worker 不可恢复(数据丢失)
psql -h coordinator <<SQL
SELECT citus_remove_node('broken-worker.internal', 5432);
SELECT citus_add_node('new-worker.internal', 5432);
SELECT citus_rebalance_start();
SQL

# 3. 监控重分布
psql -h coordinator -c "SELECT * FROM citus_rebalance_status();"

10.3 Coordinator 切换

# 方案 A: DNS 切换(推荐)
# 将 DNS A 记录从 coordinator-1 指向 coordinator-2
# TTL 设置 60s 以加速切换

# 方案 B: VIP 漂移
# keepalived 或类似工具将 VIP 从 coordinator-1 迁移到 coordinator-2

# 切换后重启所有 rustbill-server 实例(或等待连接池自动重连)
sudo systemctl restart rustbill-server

10.4 从 Citus 回退到单机 PG

# 从 Coordinator 导出全量数据
pg_dump -h coordinator.internal -U rustbill -d rustbill \
  --no-owner --no-acl -F c -f rustbill_full.dump

# 导入到单机 PG
pg_restore -h single-pg.internal -U rustbill -d rustbill \
  --no-owner --no-acl -j 4 rustbill_full.dump

# 更新 config.toml 的 DB URL
# 重启所有 rustbill-server 实例

11. 性能基准

测试环境:3 个 Citus Worker(8C32G),64 分片,100 万订单,50 个客户。

操作单机 PGCitus 3 Worker说明
客户查询自己的订单8ms9ms单 shard 查询,性能接近
管理员查询全部订单45ms62ms跨 shard 聚合,略有增加
创建订单4ms5ms单 shard 写入
支付回调(余额扣减+建支付+更新订单)3ms3ms同 shard 事务
Dashboard COUNT(6 表聚合)120ms180ms跨 shard COUNT 开销
订单列表分页(100 条/页)12ms15ms推送到 Coordinator 排序

结论: 单客户操作延迟几乎不变(+1ms)。跨客户聚合查询因 Coordinator 汇总而产生额外延迟(+30-60%),但仍在可接受范围内。


12. 部署检查清单

应用层

  • Caddy 配置 health_uri /healthhealth_interval 5s
  • Caddy lb_policy least_conn
  • [jwt].secret 所有实例一致
  • [db].url 指向同一个 Citus Coordinator 或 PG 实例
  • [redis].enabled 按需配置(多实例建议启用)
  • [telemetry].enabled 按需开启 OTLP

数据库层

  • Citus Coordinator 已添加所有 Worker 节点
  • 分布式表 customer_id 列全部 NOT NULL
  • 参考表已 create_reference_table
  • 每个 Worker 配置了至少一个 streaming replica
  • Patroni + etcd 集群正常运行

监控

  • /health 端点可被 LB 访问
  • /ready 端点配置为 k8s readiness probe
  • OTLP exporter 成功连接到 Collector
  • systemd healthcheck timer 启用
  • 告警规则配置(实例宕机、Worker 宕机、熔断器 Open)

备份

  • Coordinator 每日 pg_dump
  • Worker WAL 归档到远程存储
  • 备份恢复流程已文档化并测试

13. 运维命令速查

# === 实例管理 ===
systemctl status rustbill-server          # 实例状态
systemctl restart rustbill-server         # 重启(优雅关闭 + 启动)
journalctl -u rustbill-server -f          # 实时日志

# === Citus 集群 ===
psql -h coordinator -c "SELECT * FROM citus_get_active_worker_nodes();"
psql -h coordinator -c "SELECT count(*) FROM citus_shards;"
psql -h coordinator -c "SELECT * FROM citus_rebalance_status();"

# === Patroni ===
patronictl -c /etc/patroni/patroni.yml list          # 集群状态
patronictl -c /etc/patroni/patroni.yml switchover     # 手动切换

# === 健康检查 ===
curl -s http://localhost:50051/health
grpcurl -plaintext localhost:50051 list

# === 领导选举 ===
psql -c "SELECT role, holder, heartbeat_at, ttl_secs FROM leader_role;"

# === 熔断器状态 ===
# 通过 gRPC IntegrationService 或日志查看
journalctl -u rustbill-server | grep -i "circuit"

术语对照

术语说明
CoordinatorCitus 协调节点,接收查询并路由到 Worker
WorkerCitus 工作节点,存储实际数据分片
Shard / 分片数据分区单元,按 customer_id 哈希分配
Reference Table / 参考表复制到所有 Worker 的小表
Distributed Table / 分布式表按分片键分布到 Worker 的大表
PatroniPostgreSQL 高可用管理工具
etcd分布式键值存储,用于 Patroni 共识
TTL租约有效期(Time To Live)
Advisory LockPostgreSQL 应用层锁,用于并发控制
Saga分布式事务补偿模式

插件开发指南

RustBill 插件系统基于 Rune 脚本引擎 —— 用 Rust 原生嵌入的静态类型脚本语言编写插件,零 Rust 工具链依赖,一把文本编辑器即可开发。插件源码(.rn 文件)存储在数据库中,运行时由服务器编译并执行。

目录

  1. Rune 脚本引擎
  2. 双表模型:plugins 与 plugin_interfaces
  3. 四种插件类型
  4. Host API 参考
  5. 契约函数详解
  6. 配置展开模式
  7. 开发工作流
  8. 完整示例
  9. 常见陷阱
  10. Rune 语法速查

Rune 脚本引擎

什么是 Rune

Rune 是一个用 Rust 编写的嵌入式脚本语言,具有以下特点:

  • 静态类型:编译时类型检查,避免运行时类型错误
  • Rust 风格语法fnletasync/awaitmatchif/else 等语法接近 Rust
  • 内存安全:无 GC,通过所有权系统保证内存安全
  • 原生性能:编译为字节码在虚拟机中执行,无 FFI 开销

RustBill 使用 Rune 0.14 版本(default-features = false,仅 std, alloc, emit feature)。

执行模型:模块缓存 + 短生命周期虚拟机

编译阶段:Rune 源码 + Host API 模块 → prepare() → build() → Unit(缓存)
执行阶段:每次调用创建新 VM → 执行函数 → 销毁 VM
热重载:  编辑数据库中源码 → evict() 清除缓存 → 下次调用自动重新编译

每次函数调用的生命周期:

  1. RwLock<HashMap<ScriptKey, Unit>> 缓存中获取已编译的 Unit
  2. 基于 Unit 创建新的虚拟机(VM)
  3. 将配置和业务参数作为函数实参注入 VM
  4. 执行目标函数
  5. 获取返回值并销毁 VM

每次调用创建新 VM 的设计确保了请求间完全隔离,一个脚本的执行不会影响其他请求。

编译时验证

插件脚本在编译阶段即进行契约验证:

  • 函数存在性检查:按插件类型逐一验证所有必需函数是否已声明
  • 函数签名检查:验证参数数量(统一为 2 个参数)
  • 语法检查:Rune 编译器在 build() 阶段捕获全部语法错误

编译失败的原因会记录到服务器日志,Admin UI 插件管理页面也会展示错误信息。

零 FFI,纯原生执行

与传统的 FFI 插件系统不同,Rune 脚本的 Host API(如 HTTP 请求、SMTP 发信、MD5 签名等)是在 Rust 端实现的原生函数,在 VM 中注册为原生模块后直接调用。脚本开发者无需关心内存布局、ABI 兼容性等底层问题。


双表模型:plugins 与 plugin_interfaces

RustBill 使用两张数据库表管理插件生命周期:

plugins 表 —— 插件定义

存储从 plugins/*.rn 文件扫描到的脚本定义。无启用/禁用状态 —— 所有扫描到的插件自动存在于该表中。

列名类型说明
idUUID主键
plugin_typeTEXT插件类型(见下方枚举)
plugin_idTEXT插件标识符(如 yipaywebhook
has_admin_pageBOOLEAN是否包含 admin_page() 函数(预计算列)
versionTEXT版本号(从 fn version() 提取)

plugin_interfaces 表 —— 接口实例

插件脚本是模板,接口实例是具体配置。同一个插件定义可以创建多个接口实例,每个实例有独立的配置和启用状态。

列名类型说明
idUUID主键
plugin_def_idUUIDFK → plugins(id)
plugin_typeTEXT插件类型
plugin_idTEXT插件标识符
display_nameTEXT显示名称(用户自定义,如“主站易支付“)
configJSONB运行时配置值
config_schemaJSONB配置表单 Schema(从脚本提取)
enabledBOOLEAN是否启用
script_sourceTEXT脚本完整源码
script_hashTEXT源码哈希(用于变更检测)

唯一约束:UNIQUE(plugin_type, plugin_id, display_name)

PluginScanner —— 自动扫描同步

服务器启动时及此后每 5 分钟,PluginScanner 扫描 plugins/ 目录下的 .rn 文件:

plugins/*.rn
    │
    ▼ 扫描文件,检测新增/更新
PluginScanner
    │
    ├── 新文件 → INSERT 到 plugins 表
    ├── 已存在 → 比较版本号
    │   ├── 文件版本 > DB 版本 → UPDATE plugins + 级联同步所有子接口 config_schema
    │   └── 文件版本 <= DB 版本 → 仅更新 script_source/script_hash(保护人工修改)
    └── 已存在接口 → 自动按 plugin_type + plugin_id 链接

版本管理与级联同步

.rn 脚本中通过 fn version() 声明语义化版本号:

#![allow(unused)]
fn main() {
fn version(_config, _unused) {
    "1.0.0"
}
}

版本比较规则:

  • 文件系统版本 > DB 版本 → 更新插件定义 + 级联同步所有关联接口实例的 config_schema(确保 schema 与最新脚本一致)
  • 文件系统版本 <= DB 版本 → 仅更新 script_sourcescript_hash不同步接口(保护管理员对配置 schema 的手工修改)
  • fn version() → 版本默认为 "0.0.0"(最老,任何非零版本都触发更新)

四种插件类型

类型plugin_type DB 值说明函数数量
自建 Providerfirst_party_provider自有基础设施(KVM、Incus)12
上游 Providerupstream_provider上游分销(RustBill 上游)15
支付网关gateway支付渠道(易支付、银行转账)7
通知渠道notifier通知渠道(Webhook、邮件)6

所有四种类型均支持可选的 admin_page() 函数,用于在 Admin UI 中渲染自定义管理页面。


Host API 参考

所有 Host API 在脚本中通过 rustbill_host::函数名(参数) 调用。Rune Function trait 硬限制每次调用最多 5 个参数;所有参数和返回值均为 String 类型(或单元类型 ())。复杂数据通过 JSON 字符串传递。

日志函数

函数类型签名说明
log_info(msg)sync&str → ()记录 info 级别日志
log_warn(msg)sync&str → ()记录 warn 级别日志
log_error(msg)sync&str → ()记录 error 级别日志
#![allow(unused)]
fn main() {
rustbill_host::log_info(`Webhook sent to ${url}`);
rustbill_host::log_error(`Payment signature mismatch for ${payment_id}`);
}

HTTP 请求

函数类型签名说明
http_get(url)asyncString → StringHTTP GET,返回响应 body
http_post(url, body, content_type)async3×String → StringHTTP POST,返回响应 body
http_post_bytes(url, body)async2×String → String二进制 POST(用于 gRPC-Web)
http_post_with_headers(url, body, content_type, headers_json)async4×String → StringPOST 含自定义 Headers
http_request_with_cert(method, url, body, content_type, cert_json)async5×String → StringmTLS HTTP 请求

http_request_with_certcert_json 参数格式:

{"cert": "-----BEGIN CERTIFICATE-----\n...", "key": "-----BEGIN RSA PRIVATE KEY-----\n..."}

返回值格式:

{"status": 200, "headers": {"Content-Type": "..."}, "body": "..."}

注意http_request_with_cert 返回的是结构化 JSON,而非原始 body 字符串。需用 json_get 提取 body 字段获取实际响应体。

#![allow(unused)]
fn main() {
async fn health_check(config_json, _unused) {
    let api_url = rustbill_host::json_get(config_json, "api_url");
    let cert_pem = rustbill_host::json_get(config_json, "cert_pem");
    let key_pem = rustbill_host::json_get(config_json, "key_pem");
    let cert_json = `{"cert":"${cert_pem}","key":"${key_pem}"}`;
    let resp = rustbill_host::http_request_with_cert("GET", `${api_url}/1.0`, "", "", cert_json);
    let status = rustbill_host::json_get(resp, "status");        // HTTP 状态码
    let body = rustbill_host::json_get(resp, "body");             // 响应体
    if status == "200" { "true" } else { "false" }
}
}

加密与哈希

函数类型签名说明
md5_hex(s)sync&str → String计算 MD5 哈希并返回十六进制字符串
#![allow(unused)]
fn main() {
let params = `pid=${pid}&type=alipay&out_trade_no=${payment_id}`;
let sign = rustbill_host::md5_hex(`${params}${key}`);
}

SMTP 邮件

函数类型签名说明
smtp_send(config_json, email_json)sync2×String → ()通过 SMTP 发送邮件

config_json 格式:

{"smtp_host": "smtp.example.com", "smtp_port": "465", "from": "[email protected]", "username": "user", "password": "pass", "encryption": "ssl"}

email_json 格式:

{"to": "[email protected]", "subject": "订单确认", "body": "您的订单已确认...", "content_type": "text/plain"}

KV 状态存储

函数类型签名说明
state_get(key)asyncString → String读取 KV 状态值
state_set(key, value)async2×String → ()写入 KV 状态值

状态自动以 interface_id 为命名空间隔离,不同接口实例间的 key 互不冲突。

典型用途:支付回调去重、银行转账对账状态。

#![allow(unused)]
fn main() {
// 回调去重
let cached = rustbill_host::state_get(`cb:${payment_id}`);
if cached != "" {
    // 已处理过,直接返回
    return `{"status":"duplicate"}`;
}
rustbill_host::state_set(`cb:${payment_id}`, "1");
}

JSON 处理

函数类型签名说明
json_get(json_str, path)sync2×String → String按路径提取 JSON 字段
json_array_len(json_str, path)sync2×String → i64获取 JSON 数组长度
json_stringify(value)syncany → StringRune 值序列化为 JSON 字符串

json_get 路径语法:

路径对应 JSON Pointer说明
key/key根级字段
parent.child/parent/child嵌套字段
arr[0]/arr/0数组索引
arr[0].name/arr/0/name数组元素字段
meta.state.network.eth0.addresses[0].address/meta/state/network/eth0/addresses/0/address深层嵌套

不存在时返回空字符串 ""

#![allow(unused)]
fn main() {
let name = rustbill_host::json_get(raw, "products[0].name");
let count = rustbill_host::json_array_len(raw, "products");
let json = rustbill_host::json_stringify(#{
    payment_id: "123",
    status: "success",
});
}

契约函数详解

关键约定:所有契约函数(含 config_schemaversionhealth_check 等)必须声明 2 个参数 (config, _unused)。引擎调用时固定传入 2 个值:合并后的 JSON 配置 + 占位 unit 值。contracts.rsparam_count 统一为 2。违反此约定会导致编译错误。

返回值约定

  • Provider 适配器通过 rune::from_value::<String> 反序列化返回值,因此 所有函数返回纯字符串值(不包装 Ok/Err)。错误用 "ERROR: 描述" 前缀字符串表示。
  • Gateway/Notifier 适配器可处理 Ok/Err 类型。

PaymentGateway(支付网关)— 7 个必需函数

fn config_schema(config, _unused)                → JSON Schema 对象
fn initialize(config, _unused)                   → Ok(())
async fn create_payment(config..., payment_id, description, money)  → 支付结果对象
async fn query_payment(config..., payment_id)                       → 支付状态对象
async fn handle_callback(config..., payload)                        → 支付回调对象
async fn refund(config..., payment_id, amount)                      → 退款结果对象
fn health_check(config, _unused)                                    → Ok(true)

create_payment 返回值

#![allow(unused)]
fn main() {
#{
    gateway_payment_id: "外部支付单号",    // 必填,String
    payment_url: Some("支付跳转URL"),      // Option<String>
    qr_code: None,                         // Option<String> — 二维码数据
    instructions: None,                    // Option<String> — 支付说明
}
}

query_payment / handle_callback / refund 返回值

#![allow(unused)]
fn main() {
#{
    payment_id: "支付单号",                // 必填,String
    gateway_tx_id: None,                   // Option<String> — 网关交易流水号
    status: "success",                     // String — success / pending / failed / refunded
    paid_at: None,                         // Option<String> — 支付时间
    raw_response: Some("网关原始响应"),     // Option<String> — 调试用
}
}

Notifier(通知渠道)— 6 个必需函数

fn config_schema(config, _unused)          → JSON Schema
fn initialize(config, _unused)             → Ok(())
async fn send(config..., msg)              → Ok(())
fn supported_events(config, _unused)       → 事件类型数组 []
fn templates(config, _unused)              → 通知模板对象 #{}
fn health_check(config, _unused)           → Ok(true)

send 函数接收的 msg 是一个在调用时注入的 map,包含以下字段:

字段类型说明
event_typeString事件类型,如 "order.paid"
titleString通知标题
bodyString通知正文
recipientString接收人(通常为邮箱地址)
#![allow(unused)]
fn main() {
async fn send(merged, _unused) {
    let event_type = rustbill_host::json_get(merged, "event_type");
    let title = rustbill_host::json_get(merged, "title");
    let body = rustbill_host::json_get(merged, "body");
    let recipient = rustbill_host::json_get(merged, "recipient");

    // 构建通知 payload 并发送...
    Ok(())
}
}

supported_events 返回 [](空数组)表示监听所有事件;返回具体事件列表则仅监听列表中的事件:

#![allow(unused)]
fn main() {
fn supported_events(merged, _unused) {
    ["order.created", "order.paid", "order.cancelled", "instance.ready"]
}
}

FirstPartyProvider(自建 Provider)— 12 个必需函数

config_schema           spec_template            sync_products
initialize              query_price              create_instance
get_instance_status     terminate_instance       execute_action
health_check            instance_detail_sections  instance_actions

spec_template — 规格模板

返回 JSON 字符串(非 Rune 对象),定义用户在创建实例时可选的配置项。

#![allow(unused)]
fn main() {
fn spec_template(config, _unused) {
    `{"groups":[{...}],"fields":[{...}]}`
}
}

SpecField 结构(fields 数组和 groups[].fields 数组中的元素):

字段类型说明
keyString字段唯一标识
labelString显示标签
field_typeString类型:slider / integer / select / region / os_options
requiredBoolean是否必填
display_orderInteger排序
min / maxIntegerslider/integer 的范围
default_valueString默认值
stepInteger步长
unitString单位(如 "核""GB"
iconString图标名(cpu / memory / disk / network / server / globe / monitor
groupString所属分组 key
optionsArrayselect 类型的选项 [{value, label, description}]
descriptionString可选描述文本

SpecGroup 结构(groups 数组):

字段类型说明
keyString分组唯一标识
labelString分组显示名
display_orderInteger排序
iconString图标名
fieldsArray该分组下的字段列表

fields 顶层数组为平铺模式(无分组),groups 数组为分组模式。前端按分组优先渲染。

完整示例见下方 Incus Provider 示例

create_instance 返回值

{"instance_id": "provider-abc123", "status": "Running", "ip_address": "1.2.3.4", "created_at": ""}

get_instance_status 返回值

返回以下状态字符串之一:"running" / "stopped" / "terminated" / "pending" / "error"

instance_detail_sections 返回值

返回 JSON 字符串,是一个 section 数组。每个 section:

[
  {
    "kind": "properties",
    "title": "Server Config",
    "order": 1,
    "content_html": "<div>...</div>",
    "iframe_url": null,
    "data_json": null
  },
  {
    "kind": "iframe",
    "title": "Console",
    "order": 2,
    "content_html": null,
    "iframe_url": "https://...",
    "data_json": null
  }
]
  • content_html 渲染为 <iframe srcDoc sandbox="allow-scripts">
  • iframe_url 渲染为 <iframe src> 外部资源
  • data_json 渲染为结构化数据卡片

instance_actions 返回值

返回 JSON 字符串,是一个 action 数组:

[
  {"id": "start", "label": "Boot", "style": "Primary", "confirmation": "", "enabled": true},
  {"id": "stop", "label": "Shutdown", "style": "Default", "confirmation": "Force shutdown may cause data loss. Continue?", "enabled": true}
]

style 可选值:"Primary" / "Default" / "Danger"confirmation 为空时不弹确认框。

execute_action 返回值

{"success": true, "message": "Instance started", "data": "..."}

UpstreamProvider(上游 Provider)— 15 个必需函数

FirstPartyProvider 的全部 12 个函数 + 3 个上游专属函数:

sync_upstream_instances    — 同步上游实例列表
import_upstream_instance   — 从上游导入单个实例
get_upstream_balance       — 查询上游余额

配置展开模式

插件配置 JSON(plugin_interfaces.config)的键值在函数调用时被展开为独立的 JSON 字段,与业务参数合并后整体传入。脚本通过 rustbill_host::json_get(merged, "key") 逐一提取。

示例:假设一个网关插件的 config 为:

{
    "api_url": "https://pay.example.com",
    "pid": "1001",
    "key": "sk-xxxxxxxx"
}

当调用 create_payment 时,适配器将 config 字段与业务参数合并:

合并后的 JSON = config 字段 + business 字段

脚本中按需取值:

#![allow(unused)]
fn main() {
async fn create_payment(merged, _unused) {
    let api_url = rustbill_host::json_get(merged, "api_url");      // 来自 config
    let pid = rustbill_host::json_get(merged, "pid");              // 来自 config
    let key = rustbill_host::json_get(merged, "key");              // 来自 config
    let payment_id = rustbill_host::json_get(merged, "payment_id"); // 业务参数
    let amount = rustbill_host::json_get(merged, "amount");         // 业务参数
    // ...
}
}

每个接口实例有独立的 config 值,同一插件的不同实例(如“主站易支付“和“备用易支付“)可配置不同的 API 端点和密钥。


开发工作流

1. 编写脚本

plugins/ 目录下创建 .rn 文件。文件命名约定为 {type}-{id}.rn

  • gateway-yipay.rn — 支付网关插件,id 为 yipay
  • notifier-webhook.rn — 通知插件,id 为 webhook
  • provider-incus.rn — Provider 插件,id 为 incus

插件类型自动推断规则:

文件名模式推断类型
gateway-*.rnpayment_gateway
notifier-*.rnnotifier
provider-rustbill.rnupstream_provider(特例)
provider-*.rn(非 rustbill)first_party_provider

2. 激活脚本

两种方式:

  • 重启服务器:PluginScanner 在启动时扫描 plugins/ 目录
  • 等待自动扫描:每 5 分钟后台任务自动检测文件变更

扫描后,新插件会插入 plugins 表。到 Admin UI 的插件管理页面即可看到新条目。

3. 创建接口实例

通过 Admin UI → 插件管理:

  1. 选择插件定义 → 点击“创建接口“
  2. 输入显示名称(如“主站易支付“)
  3. 系统自动从脚本提取 config_schema 并存入 plugin_interfaces.config_schema
  4. 接口创建后,在配置标签页编辑 config JSON 填入实际参数

4. 启用并使用

  1. 将接口的 enabled 设为 true
  2. 接口立即注册到 PluginRegistry,随后业务逻辑即可引用该接口
  3. 例如,在商品管理中关联某个 Provider 接口,或在通知管理中启用某个 Notifier 接口

5. 热更新脚本

  1. 在 Admin UI 插件管理页的“源码“标签页编辑脚本
  2. 保存后系统自动调用 ScriptEngine.evict(key) 清除缓存
  3. 下一次函数调用触发时自动重新编译
  4. 无需重启服务器,零停机更新

6. 完整生命周期

┌─────────────┐     ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│  编写 .rn   │────▶│ 扫描/插入   │────▶│ 创建接口    │────▶│  启用使用   │
│  脚本文件   │     │ plugins 表  │     │ 实例+配置   │     │  Registry   │
└─────────────┘     └─────────────┘     └─────────────┘     └─────────────┘
                                                                  │
                                                                  ▼
┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│  重新编译   │◀────│ 清除缓存    │◀────│ 编辑源码    │
│  即插即用   │     │   evict()   │     │ Admin UI    │
└─────────────┘     └─────────────┘     └─────────────┘

完整示例

示例一:PaymentGateway(简易支付网关)

以下是一个完整的支付网关插件,模拟在线支付跳转逻辑:

#![allow(unused)]
fn main() {
// gateway-simplepay.rn — 简易支付网关示例
// 所有函数接收 (merged_json, _unused),config 字段与业务字段合并传入

fn config_schema(_config, _unused) {
    #{
        type: "object",
        required: ["api_url", "app_id", "app_secret"],
        properties: #{
            api_url: #{type: "string", description: "支付接口地址"},
            app_id: #{type: "string", description: "应用ID"},
            app_secret: #{type: "string", description: "应用密钥", format: "password"},
            notify_url: #{type: "string", description: "异步通知地址"},
            return_url: #{type: "string", description: "同步跳转地址(可选)"},
        },
    }
}

fn version(_config, _unused) {
    "1.0.0"
}

fn initialize(merged, _unused) {
    Ok(())
}

// 创建支付订单
async fn create_payment(merged, _unused) {
    // 从 config 提取
    let api_url = rustbill_host::json_get(merged, "api_url");
    let app_id = rustbill_host::json_get(merged, "app_id");
    let app_secret = rustbill_host::json_get(merged, "app_secret");
    let notify_url = rustbill_host::json_get(merged, "notify_url");
    let return_url = rustbill_host::json_get(merged, "return_url");

    // 从业务参数提取
    let payment_id = rustbill_host::json_get(merged, "payment_id");
    let description = rustbill_host::json_get(merged, "description");
    let amount = rustbill_host::json_get(merged, "amount");

    // 构建签名
    let raw = `${app_id}${payment_id}${amount}${app_secret}`;
    let sign = rustbill_host::md5_hex(raw);

    // 构建 POST body
    let body = `app_id=${app_id}&out_trade_no=${payment_id}&name=${description}&money=${amount}&notify_url=${notify_url}&return_url=${return_url}&sign=${sign}`;

    // 调用上游 API
    let resp = rustbill_host::http_post(`${api_url}/api/create`, body, "application/x-www-form-urlencoded");

    // 解析跳转 URL
    let payment_url = if resp.starts_with("http") { resp } else { `${api_url}/pay/${payment_id}` };
    rustbill_host::log_info(`Payment created: ${payment_id}`);

    rustbill_host::json_stringify(#{
        gateway_payment_id: payment_id,
        payment_url: Some(payment_url),
        qr_code: None,
        instructions: None,
    })
}

// 查询支付状态
async fn query_payment(merged, _unused) {
    let api_url = rustbill_host::json_get(merged, "api_url");
    let app_id = rustbill_host::json_get(merged, "app_id");
    let app_secret = rustbill_host::json_get(merged, "app_secret");
    let payment_id = rustbill_host::json_get(merged, "payment_id");

    let body = `app_id=${app_id}&out_trade_no=${payment_id}`;
    let resp = rustbill_host::http_post(`${api_url}/api/query`, body, "application/x-www-form-urlencoded");

    let status = if resp.contains("SUCCESS") { "success" } else if resp.contains("FAIL") { "failed" } else { "pending" };

    rustbill_host::json_stringify(#{
        payment_id,
        gateway_tx_id: None,
        status,
        paid_at: None,
        raw_response: Some(resp),
    })
}

// 处理异步回调
async fn handle_callback(merged, _unused) {
    let payload = rustbill_host::json_get(merged, "payload");
    let payment_id = rustbill_host::json_get(merged, "payment_id");
    let app_secret = rustbill_host::json_get(merged, "app_secret");

    // 验证签名
    let sign = extract_param(payload, "sign");
    let base = strip_sign(payload);
    let expected = rustbill_host::md5_hex(`${base}${app_secret}`);
    if sign != expected {
        rustbill_host::log_error(`Callback signature mismatch for ${payment_id}`);
        return "ERROR: invalid signature";
    }

    // 回调去重
    let cached = rustbill_host::state_get(`cb:${payment_id}`);
    if cached != "" {
        rustbill_host::log_info(`Duplicate callback ignored: ${payment_id}`);
        return rustbill_host::json_stringify(#{
            payment_id,
            gateway_tx_id: None,
            status: "success",
            paid_at: None,
            raw_response: Some("duplicate"),
        });
    }
    rustbill_host::state_set(`cb:${payment_id}`, "1");

    rustbill_host::json_stringify(#{
        payment_id,
        gateway_tx_id: None,
        status: "success",
        paid_at: None,
        raw_response: Some(payload),
    })
}

// 发起退款
async fn refund(merged, _unused) {
    let api_url = rustbill_host::json_get(merged, "api_url");
    let app_id = rustbill_host::json_get(merged, "app_id");
    let app_secret = rustbill_host::json_get(merged, "app_secret");
    let payment_id = rustbill_host::json_get(merged, "payment_id");
    let amount = rustbill_host::json_get(merged, "amount");

    let body = `app_id=${app_id}&out_trade_no=${payment_id}&money=${amount}&app_secret=${app_secret}`;
    let resp = rustbill_host::http_post(`${api_url}/api/refund`, body, "application/x-www-form-urlencoded");
    rustbill_host::log_info(`Refund requested: ${payment_id} ${amount}`);

    rustbill_host::json_stringify(#{
        payment_id,
        gateway_tx_id: None,
        status: "refunded",
        paid_at: None,
        raw_response: Some(resp),
    })
}

// 健康检查
fn health_check(merged, _unused) {
    Ok(true)
}

// ── 辅助函数 ──

fn extract_param(query, name) {
    let prefix = `${name}=`;
    let start = query.find(prefix);
    if start.is_none() { return ""; }
    let begin = start.unwrap() + prefix.len();
    let rest = query[begin:];
    let end = rest.find("&");
    if end.is_some() { rest[0:end.unwrap()] } else { rest }
}

fn strip_sign(query) {
    let idx = query.find("&sign=");
    if idx.is_some() { query[0:idx.unwrap()] } else { query }
}
}

示例二:FirstPartyProvider(简化版)

#![allow(unused)]
fn main() {
// provider-example.rn — 自建 Provider 示例

fn config_schema(_config, _unused) {
    `{"type":"object","required":["api_url"],"properties":{"api_url":{"type":"string","description":"API 地址"},"api_token":{"type":"string","description":"API Token","format":"password"}}}`
}

fn version(_config, _unused) {
    "1.0.0"
}

fn initialize(merged, _unused) {
    "OK"
}

// 规格模板 — 定义用户可选配置项
fn spec_template(config, _unused) {
    `{
        "groups": [
            {
                "key": "compute",
                "label": "计算资源",
                "display_order": 1,
                "icon": "cpu",
                "fields": [
                    {
                        "key": "cpu_cores",
                        "label": "CPU 核数",
                        "field_type": "slider",
                        "required": true,
                        "display_order": 1,
                        "min": 1,
                        "max": 64,
                        "default_value": "2",
                        "step": 1,
                        "unit": "核",
                        "icon": "cpu",
                        "group": "compute",
                        "options": []
                    },
                    {
                        "key": "memory_gb",
                        "label": "内存",
                        "field_type": "slider",
                        "required": true,
                        "display_order": 2,
                        "min": 1,
                        "max": 512,
                        "default_value": "4",
                        "step": 1,
                        "unit": "GB",
                        "icon": "memory",
                        "group": "compute",
                        "options": []
                    }
                ]
            },
            {
                "key": "location",
                "label": "区域位置",
                "display_order": 2,
                "icon": "globe",
                "fields": [
                    {
                        "key": "region",
                        "label": "区域",
                        "field_type": "region",
                        "required": true,
                        "display_order": 1,
                        "unit": "",
                        "icon": "globe",
                        "group": "location",
                        "options": []
                    }
                ]
            }
        ]
    }`
}

async fn sync_products(config_json, _unused) {
    let api_url = rustbill_host::json_get(config_json, "api_url");
    let api_token = rustbill_host::json_get(config_json, "api_token");
    let headers = `{"Authorization":"Bearer ${api_token}","Content-Type":"application/json"}`;
    let raw = rustbill_host::http_post_with_headers(`${api_url}/products`, "{}", "application/json", headers);
    raw  // 返回原始响应,由适配器解析
}

async fn query_price(config_json, _unused) {
    `{"amount":"0.00","currency":"CNY","billing_cycle":"monthly"}`
}

async fn create_instance(config_json, _unused) {
    let api_url = rustbill_host::json_get(config_json, "api_url");
    let order_id = rustbill_host::json_get(config_json, "order_id");
    let cpu = rustbill_host::json_get(config_json, "cpu_cores");
    let mem = rustbill_host::json_get(config_json, "memory_gb");

    rustbill_host::log_info(`Creating instance for order ${order_id}: ${cpu} cores, ${mem} GB`);
    // ... 调用底层 API 创建实例 ...

    `{"instance_id":"inst-${order_id}","status":"Running","ip_address":"1.2.3.4","created_at":""}`
}

async fn get_instance_status(config_json, _unused) {
    let api_url = rustbill_host::json_get(config_json, "api_url");
    let instance_id = rustbill_host::json_get(config_json, "instance_id");

    // ... 查询实例状态 ...
    "running"
}

async fn terminate_instance(config_json, _unused) {
    let instance_id = rustbill_host::json_get(config_json, "instance_id");
    rustbill_host::log_info(`Terminating instance ${instance_id}`);
    ""
}

async fn execute_action(config_json, _unused) {
    let instance_id = rustbill_host::json_get(config_json, "instance_id");
    let action = rustbill_host::json_get(config_json, "action");
    rustbill_host::log_info(`Executing ${action} on ${instance_id}`);
    ""
}

fn health_check(config_json, _unused) {
    "true"
}

fn instance_detail_sections(instance_json, _unused) {
    let cpu = rustbill_host::json_get(instance_json, "server_spec.cpu_cores");
    let mem = rustbill_host::json_get(instance_json, "server_spec.memory_gb");

    `[{"kind":"properties","title":"Server Info","order":1,"content_html":"<div>CPU: ${cpu} cores, Memory: ${mem} GB</div>","iframe_url":null,"data_json":null}]`
}

fn instance_actions(instance_json, _unused) {
    let status = rustbill_host::json_get(instance_json, "status");
    if status == "running" {
        `[{"id":"stop","label":"Shutdown","style":"Default","confirmation":"Are you sure?","enabled":true},{"id":"reboot","label":"Reboot","style":"Default","confirmation":"","enabled":true}]`
    } else {
        `[{"id":"start","label":"Boot","style":"Primary","confirmation":"","enabled":true}]`
    }
}
}

示例三:Notifier(Webhook 通知)

#![allow(unused)]
fn main() {
// notifier-webhook.rn — Webhook 通知插件

fn config_schema(_config, _unused) {
    #{
        type: "object",
        required: ["url"],
        properties: #{
            url: #{type: "string", description: "Webhook URL"},
            secret: #{type: "string", description: "签名密钥(可选)"},
        },
    }
}

fn version(_config, _unused) {
    "1.0.0"
}

fn initialize(merged, _unused) {
    Ok(())
}

// 监听所有事件(返回空数组)
fn supported_events(merged, _unused) {
    []
}

// 通知模板(空对象表示使用系统默认模板)
fn templates(merged, _unused) {
    #{}
}

async fn send(merged, _unused) {
    let url = rustbill_host::json_get(merged, "url");
    let event_type = rustbill_host::json_get(merged, "event_type");
    let title = rustbill_host::json_get(merged, "title");
    let body = rustbill_host::json_get(merged, "body");
    let recipient = rustbill_host::json_get(merged, "recipient");

    let payload = `{"event_type":"${event_type}","title":"${title}","body":"${body}","recipient":"${recipient}"}`;
    rustbill_host::http_post(url, payload, "application/json");
    rustbill_host::log_info(`Webhook sent to ${url}: ${event_type}`);
    Ok(())
}

fn health_check(merged, _unused) {
    Ok(true)
}
}

示例四:Notifier(邮件通知)

#![allow(unused)]
fn main() {
// notifier-email.rn — 邮件通知插件

fn config_schema(_config, _unused) {
    #{
        type: "object",
        required: ["smtp_host", "from"],
        properties: #{
            smtp_host: #{type: "string", description: "SMTP 服务器地址"},
            smtp_port: #{type: "integer", description: "SMTP 端口", default_value: "465"},
            from: #{type: "string", description: "发件人邮箱"},
            username: #{type: "string", description: "SMTP 用户名(可选)"},
            password: #{type: "string", description: "SMTP 密码(可选)", format: "password"},
            encryption: #{type: "string", description: "加密方式", enum: ["starttls", "ssl", "none"], default_value: "ssl"},
        },
    }
}

fn version(_config, _unused) {
    "1.0.0"
}

fn initialize(merged, _unused) {
    Ok(())
}

fn supported_events(merged, _unused) {
    ["order.created", "order.paid", "order.cancelled", "instance.ready", "billing.overdue"]
}

fn templates(merged, _unused) {
    #{}
}

async fn send(merged, _unused) {
    // 将 config 字段序列化为 SMTP 配置 JSON
    let config = rustbill_host::json_stringify(#{
        smtp_host: rustbill_host::json_get(merged, "smtp_host"),
        smtp_port: rustbill_host::json_get(merged, "smtp_port"),
        from: rustbill_host::json_get(merged, "from"),
        username: rustbill_host::json_get(merged, "username"),
        password: rustbill_host::json_get(merged, "password"),
        encryption: rustbill_host::json_get(merged, "encryption"),
    });

    // 将 msg 字段序列化为邮件 JSON
    let email = rustbill_host::json_stringify(#{
        to: rustbill_host::json_get(merged, "recipient"),
        subject: rustbill_host::json_get(merged, "title"),
        body: rustbill_host::json_get(merged, "body"),
        content_type: "text/plain",
    });

    rustbill_host::smtp_send(config, email);
    Ok(())
}

fn health_check(merged, _unused) {
    Ok(true)
}
}

示例五:config_schema 详解

config_schema 返回 JSON Schema 对象,Admin UI 据此渲染配置表单。支持以下字段类型:

类型说明对应表单控件
string字符串文本输入框
string + format: "password"密码密码输入框
string + enum枚举下拉选择框
integer整数数字输入框
number浮点数数字输入框

完整示例:

#![allow(unused)]
fn main() {
fn config_schema(_config, _unused) {
    #{
        type: "object",
        required: ["api_url", "api_key", "merchant_id"],
        properties: #{
            api_url: #{type: "string", description: "API 端点地址", default_value: "https://api.example.com"},
            api_key: #{type: "string", description: "API 密钥", format: "password"},
            merchant_id: #{type: "string", description: "商户号"},
            timeout: #{type: "integer", description: "请求超时(秒)", default_value: "30", minimum: "1", maximum: "300"},
            currency: #{type: "string", description: "结算货币", enum: ["CNY", "USD", "EUR"], default_value: "CNY"},
            sandbox: #{type: "boolean", description: "沙箱模式", default_value: "false"},
        },
    }
}
}

常见陷阱

函数签名

  1. 所有函数必须声明 2 个参数 (config, _unused)contracts.rsparam_count 统一为 2。引擎调用时固定传入 2 个值,写 1 个参数会导致编译失败。

  2. 函数名必须与契约完全一致。例如 create_payment 不是 createPayment。拼写错误或大小写不匹配会被编译器报告为“缺少必需函数“。

  3. 参数数量必须匹配契约。每个插件类型有固定的必需函数集(Gateway 7 个、Notifier 6 个、Provider 12/15 个),缺少任何一个都会导致编译失败。

返回值

  1. Provider 函数返回纯字符串值(不包装 Ok/Err)。Provider 适配器通过 rune::from_value::<String> 反序列化返回值。返回 Ok(xxx) 会被反序列化失败。错误用 "ERROR:描述" 前缀。

  2. 返回值使用 Rune 对象字面量 #{},不是 JSON 字符串 "{}"#{key: "val"} 是 Rune 对象,在 Rune 中是原生数据结构;"{\"key\":\"val\"}" 是字符串,类型不同。

  3. 返回复杂数据(数组、嵌套对象)时用 json_stringify()。Rune 对象和数组不能直接跨 FFI 边界传递(除了通过 JSON 序列化)。例如:

#![allow(unused)]
fn main() {
// 正确 — Provider 返回值
rustbill_host::json_stringify(#{
    gateway_payment_id: payment_id,
    payment_url: Some(url),
})

// 错误 — 不能直接返回 Rune 数组
[1, 2, 3]  // 适配器无法反序列化
}

Rune 语法

  1. Rune 是静态类型。对象字面量用 #{key: value},不是 JavaScript 的 {key: value}#{} 创建的是 Rune object 类型。

  2. 字符串插值用反引号(backticks)。`text ${var} text`,不是 JavaScript 的模板字符串。

  3. 变量必须先声明后使用。与 Rust 相同,let 声明变量。Rune 不支持 var

  4. Option 值用 Some(val) / NoneSome(val) 返回一个带值的 Option,None 返回空 Option。序列化为 JSON 后分别变为 "val"null

Host API 限制

  1. Host API 函数最多 5 个参数(Rune Function trait 硬限制)。超过 5 个参数需合并为 JSON 字符串。如 http_request_with_certcert_json 参数将证书和密钥合并为一个 JSON 字符串。

  2. Host API 参数和返回值全是 String 类型(少数返回 ()i64)。不能用 i32f32 等非 String 类型传递数据。数字通过 String 传递,脚本内部按需 .parse::<f64>().parse::<i64>()

热重载与调试

  1. 热重载不生效:检查 Admin UI 是否在保存源码后触发了 evict()。保存按钮会自动处理此逻辑。手动修改数据库需要自行调用 evict。

  2. 编译错误排查:查看服务器日志(RUST_LOG=rustbill_server=debug)。编译错误信息包含具体的错误函数名和原因。

  3. 运行时错误排查:使用 rustbill_host::log_info/log_error 在脚本中加入调试日志。日志会输出到服务器日志流中。

配置和部署

  1. config_schema 必须是合法的 JSON Schema(至少含 typeproperties 键)。最小合法 schema:{"type":"object","properties":{}}

  2. plugins/ 目录路径由 config.toml[plugins].plugins_dir 配置,默认为 "./plugins"

  3. Admin Page 功能的 admin_page() 是可选函数。所有四种插件类型均支持。存在该函数时,plugins.has_admin_page 设为 true,Admin UI 在插件管理页渲染自定义管理页面。


Rune 语法速查

以下涵盖插件开发中最常用的 Rune 语法:

变量与基本类型

#![allow(unused)]
fn main() {
// 变量声明
let x = 42;
let msg = "hello";
let flag = true;
let nothing = ();              // 单元类型

// 字符串插值
let url = `https://api.example.com/v1/${resource}`;

// 数字类型
let count: i64 = 100;         // 有符号 64 位整数
let price: f64 = 29.99;       // 64 位浮点数
let n = "42".parse::<i64>().unwrap_or(0);
}

条件与循环

#![allow(unused)]
fn main() {
if status == "running" {
    "active"
} else if status == "stopped" {
    "inactive"
} else {
    "unknown"
}

let mut i = 0;
while i < count {
    // ...
    i = i + 1;
}
}

对象与数组

#![allow(unused)]
fn main() {
// 对象字面量
let obj = #{key: "value", count: 42, enabled: true};

// 数组字面量
let arr = ["a", "b", "c"];

// Option
let maybe: Option = Some("present");
let nothing: Option = None;
}

函数定义

#![allow(unused)]
fn main() {
// 同步函数
fn add(a, b) {
    a + b
}

// 异步函数
async fn fetch_data(url) {
    let resp = rustbill_host::http_get(url);
    rustbill_host::log_info(`Fetched ${url}`);
    resp
}

// 带返回值的函数 — 最后一个表达式即为返回值
fn check(merged, _unused) {
    let val = rustbill_host::json_get(merged, "key");
    if val == "good" {
        "OK"
    } else {
        "ERROR:bad value"
    }
}
}

字符串与 Option 操作

#![allow(unused)]
fn main() {
// 字符串查找
let pos = "hello world".find("world");

// 字符串切片
let sub = "hello"[0:2];          // "he"

// 字符串拼接
let combined = `prefix_${id}`;

// 空字符串检查
if val == "" { /* ... */ }
if val.is_empty() { /* ... */ }

// Option unwrap
let v = opt.unwrap_or("default");

// 字符串长度
let n = s.len();
}

错误处理

#![allow(unused)]
fn main() {
// 返回错误(Gateway/Notifier 可用)
return Err("something went wrong");

// Provider 错误返回约定
return "ERROR: something went wrong";

// 值不存在时返回默认值
let val = rustbill_host::json_get(raw, "path");
if val == "" {
    return "ERROR:missing required field";
}
}

附录:已实现的插件参考

以下是 RustBill 仓库中已实现的 6 个插件,可作为开发参考:

文件类型说明
plugins/gateway-yipay.rnPaymentGateway易支付聚合支付 — MD5 签名 + HTTP
plugins/gateway-banktransfer.rnPaymentGateway银行转账 — 本地逻辑 + KV 对账
plugins/notifier-webhook.rnNotifierWebhook HTTP POST 通知
plugins/notifier-email.rnNotifierSMTP 邮件通知(lettre)
plugins/provider-rustbill.rnUpstreamProviderRustBill 上游分销 — gRPC-Web
plugins/provider-incus.rnFirstPartyProviderIncus 虚拟化 — mTLS REST API

所有脚本源码均可通过 Admin UI 插件管理页面的“源码“标签页在线查看和编辑。

gRPC API 参考

RustBill 使用 gRPC 作为后端 API 协议,同时支持 gRPC-Web(浏览器端)。共 10 个 gRPC 服务,分布在 6 个 proto package 中。

通用约定

约定说明
金额所有金额字段使用字符串类型(如 "50.00"),不使用浮点数
ID所有 ID 均为 UUID v7 字符串
时间戳RFC3339 格式字符串(如 "2024-01-15T10:30:00Z"
分页PageRequest { page: uint32, page_size: uint32 },响应含 PageMeta
可选字段proto3 optional 关键字标记的字段,未设置时不出现在 JSON payload 中

认证机制

RustBill 使用三种认证方式,分别对应不同的用户类型和中间件:

认证方式用户类型Token 格式中间件
Session CookieAdminhttpOnly CookieSessionAuthLayer
JWT BearerCustomerAuthorization: Bearer <token>JwtAuthLayer
API Key BearerDownstreamAuthorization: Bearer <api_key>ApiKeyAuthLayer

Admin 认证流程:登录 → 服务端返回 Set-Cookie → 浏览器自动携带 cookie。Admin SPA 使用 credentials: 'include' 发送同源请求。

Customer 认证流程:登录 → 返回 access_token + refresh_token → 前端存储到 localStorage → 每次请求在 Authorization 头携带。

API Key 认证流程:在 ApiKeyService 创建 → 在 Authorization 头中作为 Bearer token 携带 → 注入 DownstreamUser

gRPC 服务路径

所有 gRPC 方法遵循路径格式:

/rustbill.{package}.{Service}/{Method}

例如:

  • /rustbill.identity.IdentityService/Login
  • /rustbill.product.ProductService/ListProducts
  • /rustbill.integration.IntegrationService/ListPlugins

gRPC-Web

浏览器端使用 gRPC-Web 协议。请求需携带 grpc-web 头,响应可能为 trailers-only(状态码 200 + grpc-status header)。CORS 需服务端配置允许的 origin。

权限级别

权限级别说明
公开无需认证
已认证admin 或 customer 任一认证通过即可
admin需 admin 登录(Admin 或 Operator 角色)
admin-only仅 Admin 角色(不含 Operator)
customercustomer 用户自动限定数据范围

服务清单

服务Package文件职责
IdentityServicerustbill.identityidentity.md认证、用户管理、密码管理
ProductServicerustbill.productproduct.md商品 CRUD、批量操作、上游导入
CustomerServicerustbill.customercustomer.md客户 CRUD
OrderServicerustbill.orderorder.md订单全生命周期、支付
BillingServicerustbill.billingbilling.md发票管理、周期账单生成
PaymentServicerustbill.paymentpayment.md支付创建、回调、退款
IntegrationServicerustbill.integrationintegration.md插件管理、供应商操作、上游实例
NotificationServicerustbill.notificationnotification.md通知渠道、手动发送
TicketServicerustbill.ticketticket.md工单全生命周期管理
ApiKeyServicerustbill.downstreamapikey.mdAPI Key 创建与管理

公共类型

定义在 rustbill.common package(common.proto)。

PageRequest

字段类型编号描述
pageuint321页码(从 1 开始)
page_sizeuint322每页条数

PageMeta

字段类型编号描述
totaluint641总记录数
pageuint322当前页码
page_sizeuint323每页条数
total_pagesuint324总页数

EmptyRequest / EmptyResponse

无字段的空消息。

IdRequest

字段类型编号描述
idstring1UUID v7 格式 ID

错误码

所有 gRPC 错误通过标准 grpc-status header 返回:

错误码gRPC Status说明
InvalidArgument3参数校验失败
NotFound5资源不存在
AlreadyExists6资源已存在(重复创建)
PermissionDenied7权限不足
Unauthenticated16未认证(需登录)
Internal13服务器内部错误
Unimplemented12功能未实现
Unavailable14服务不可用

IdentityService

用户认证与管理的核心服务,负责注册、登录、用户 CRUD、密码管理。支持 Admin 和 Customer 两种用户类型,认证方式分别为 Session Cookie(Admin)和 JWT Bearer(Customer)。

RPC 列表

RPC描述权限
Register注册新用户(自动创建关联 Customer)公开
SendVerificationCode发送邮箱验证码公开
Login登录(admin 返回 session cookie,customer 返回 JWT)公开
RefreshToken刷新 access token(仅 customer)公开
ListUsers用户列表(分页+筛选)admin
GetUser获取单个用户admin
UpdateUser更新用户信息admin
DeleteUser删除用户admin-only
ChangePassword修改自己的密码已认证
AdminResetPassword管理员重置他人密码admin-only
GetMe获取当前登录用户信息已认证
Logout登出(admin 清除 session,customer 客户端删 token)已认证

Register

描述

注册新 customer 用户,自动创建关联的 Customer 记录。

Request — RegisterRequest

字段类型编号必填描述
usernamestring1用户名
emailstring2邮箱地址
display_namestring3显示名称
passwordstring4登录密码
verification_codestring5邮箱验证码

Response — RegisterResponse

字段类型编号描述
userUserInfo1新创建的用户信息
customer_idstring2自动创建的关联 Customer ID

错误码

错误说明
InvalidArgument验证码无效或已过期、用户名已存在、密码不符合要求
AlreadyExists邮箱已被注册

示例

grpcurl -plaintext \
  -d '{"username":"testuser","email":"[email protected]","display_name":"Test","password":"Pass123!","verification_code":"123456"}' \
  localhost:50051 rustbill.identity.IdentityService/Register
const resp = await api.register({
  username: "testuser",
  email: "[email protected]",
  display_name: "Test",
  password: "Pass123!",
  verification_code: "123456"
});
// resp.user, resp.customer_id

SendVerificationCode

描述

向指定邮箱发送验证码,用于注册或密码重置流程。

Request — SendVerificationCodeRequest

字段类型编号必填描述
emailstring1接收验证码的邮箱
purposestring2用途:"registration""password_reset"

Response — SendVerificationCodeResponse

字段类型编号描述
sentbool1是否发送成功
messagestring2提示信息
retry_after_secsint323多少秒后可重新发送

错误码

错误说明
InvalidArgument邮箱格式无效、发送频率过高
Internal邮件服务不可用

示例

grpcurl -plaintext \
  -d '{"email":"[email protected]","purpose":"registration"}' \
  localhost:50051 rustbill.identity.IdentityService/SendVerificationCode
const resp = await api.sendVerificationCode({
  email: "[email protected]",
  purpose: "registration"
});
// resp.sent, resp.retry_after_secs

Login

描述

用户登录。user_type="admin" 时返回 session cookie(Set-Cookie 响应头),user_type="customer" 时返回 JWT token 对。

Request — LoginRequest

字段类型编号必填描述
usernamestring1用户名
passwordstring2密码
user_typestring3用户类型:"admin""customer"

Response — LoginResponse

字段类型编号描述
access_tokenstring1JWT access token(仅 customer)
refresh_tokenstring2JWT refresh token(仅 customer)
expires_inint643token 过期时间(秒)
userUserInfo4当前用户信息
admin_pathstring5Admin 面板路径(来自服务端配置)

错误码

错误说明
InvalidArgument用户名或密码错误
PermissionDenied用户已禁用
NotFound用户不存在

示例

# Admin 登录
grpcurl -plaintext \
  -d '{"username":"admin","password":"admin123","user_type":"admin"}' \
  localhost:50051 rustbill.identity.IdentityService/Login

# Customer 登录
grpcurl -plaintext \
  -d '{"username":"customer1","password":"pass123","user_type":"customer"}' \
  localhost:50051 rustbill.identity.IdentityService/Login
const resp = await api.login({
  username: "customer1",
  password: "pass123",
  user_type: "customer"
});
// resp.access_token → 存入 localStorage
// resp.user → 存入 auth store

RefreshToken

描述

使用 refresh token 获取新的 access token(仅 customer 用户)。

Request — RefreshTokenRequest

字段类型编号必填描述
refresh_tokenstring1有效的 refresh token

Response — RefreshTokenResponse

字段类型编号描述
access_tokenstring1新的 access token
expires_inint642过期时间(秒)
refresh_tokenstring3新的 refresh token(轮换)

错误码

错误说明
InvalidArgumentrefresh token 无效或已过期

示例

grpcurl -plaintext \
  -d '{"refresh_token":"eyJhbGciOiJIUzI1NiIs..."}' \
  localhost:50051 rustbill.identity.IdentityService/RefreshToken
const resp = await api.refreshToken({
  refresh_token: localStorage.getItem("rustbill_customer_refresh")
});
// 更新 localStorage 中的 token

ListUsers

描述

分页查询用户列表,支持按角色、状态、用户类型、关键词、客户 ID 筛选。

Request — ListUsersRequest

字段类型编号必填描述
paginationPageRequest1分页参数
rolestring?2角色过滤("admin" / "operator"
is_activebool?3启用状态过滤
searchstring?4用户名/邮箱搜索
user_typestring?5用户类型:"admin""customer",不填默认 admin
customer_idstring?6按关联客户过滤(仅 user_type=“customer” 时有效)

Response — ListUsersResponse

字段类型编号描述
usersrepeated UserInfo1用户列表
metaPageMeta2分页元数据

错误码

错误说明
PermissionDenied非 admin 用户

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"pagination":{"page":1,"page_size":20},"user_type":"customer","is_active":true}' \
  localhost:50051 rustbill.identity.IdentityService/ListUsers
const resp = await api.listUsers({
  pagination: { page: 1, page_size: 20 },
  user_type: "customer",
  is_active: true
});

GetUser

描述

获取单个用户的详细信息。

Request — GetUserRequest

字段类型编号必填描述
idstring1用户 ID(UUID)
user_typestring?2用户类型提示,避免回退查询

Response — GetUserResponse

字段类型编号描述
userUserInfo1用户信息

错误码

错误说明
NotFound用户不存在
PermissionDenied非 admin 用户

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"id":"0192a123-4567-7890-abcd-ef0123456789"}' \
  localhost:50051 rustbill.identity.IdentityService/GetUser

UpdateUser

描述

更新用户信息(邮箱、显示名称、角色、状态等)。

Request — UpdateUserRequest

字段类型编号必填描述
idstring1用户 ID
emailstring?2新邮箱
display_namestring?3新显示名称
rolestring?4新角色(仅 user_type=“admin” 时有效)
is_activebool?5启用/禁用
user_typestring?6用户类型
customer_idstring?7关联客户 ID(仅 user_type=“customer” 时有效)

Response — UpdateUserResponse

字段类型编号描述
userUserInfo1更新后的用户信息

错误码

错误说明
NotFound用户不存在
InvalidArgument参数校验失败
PermissionDenied非 admin 用户

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"id":"0192a123-...","display_name":"New Name","is_active":true}' \
  localhost:50051 rustbill.identity.IdentityService/UpdateUser

DeleteUser

描述

删除指定用户(仅 Admin 角色可操作)。

Request — DeleteUserRequest

字段类型编号必填描述
idstring1用户 ID(UUID)

Response — DeleteUserResponse

无字段。

错误码

错误说明
NotFound用户不存在
PermissionDenied非 Admin 角色

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"id":"0192a123-4567-7890-abcd-ef0123456789"}' \
  localhost:50051 rustbill.identity.IdentityService/DeleteUser

ChangePassword

描述

当前登录用户修改自己的密码(需提供旧密码验证)。

Request — ChangePasswordRequest

字段类型编号必填描述
old_passwordstring1当前密码
new_passwordstring2新密码

Response — ChangePasswordResponse

无字段。

错误码

错误说明
InvalidArgument旧密码错误、新密码不符合要求
Unauthenticated未登录

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"old_password":"oldpass","new_password":"newpass123!"}' \
  localhost:50051 rustbill.identity.IdentityService/ChangePassword
const resp = await api.changePassword({
  old_password: "oldpass",
  new_password: "newpass123!"
});

AdminResetPassword

描述

管理员重置指定用户的密码(仅 Admin 角色可操作,无需旧密码)。

Request — AdminResetPasswordRequest

字段类型编号必填描述
user_idstring1目标用户 ID
new_passwordstring2新密码

Response — AdminResetPasswordResponse

无字段。

错误码

错误说明
NotFound用户不存在
PermissionDenied非 Admin 角色

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"user_id":"0192a123-...","new_password":"resetpass123!"}' \
  localhost:50051 rustbill.identity.IdentityService/AdminResetPassword

GetMe

描述

获取当前登录用户的个人信息。Admin 用户从 session cookie 恢复身份,Customer 用户从 JWT 解析。

Request — GetMeRequest

无字段。

Response — GetMeResponse

字段类型编号描述
userUserInfo1当前用户信息

错误码

错误说明
Unauthenticated未登录

示例

# Admin: cookie 自动携带
grpcurl -plaintext -H "cookie: rustbill_session=..." \
  -d '{}' \
  localhost:50051 rustbill.identity.IdentityService/GetMe

# Customer: JWT Bearer
grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{}' \
  localhost:50051 rustbill.identity.IdentityService/GetMe
// Admin SPA (credentials: 'include' 自动携带 cookie)
const resp = await api.getMe({});
// Customer SPA
const resp = await api.getMe({});

Logout

描述

登出当前用户。Admin 端清除 session cookie,Customer 端客户端自行删除本地 token。

Request — LogoutRequest

无字段。

Response — LogoutResponse

无字段。

错误码

错误说明
Unauthenticated未登录

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{}' \
  localhost:50051 rustbill.identity.IdentityService/Logout
const resp = await api.logout({});
// 清除 localStorage 中的 token
localStorage.removeItem("rustbill_customer_token");
localStorage.removeItem("rustbill_customer_refresh");

公共类型

UserInfo

字段类型编号描述
idstring1用户 UUID
usernamestring2用户名
emailstring3邮箱地址
display_namestring4显示名称
rolestring5角色:"admin" / "operator"(admin 用户);空字符串(customer 用户)
is_activebool6是否启用
created_atstring7创建时间(RFC3339)
user_typestring8用户类型:"admin" / "customer"
customer_idstring9关联的 Customer ID(admin 用户为空字符串)

ProductService

商品管理服务,负责商品的 CRUD、批量操作、上游供应商产品同步与导入、批量定价。

RPC 列表

RPC描述权限
CreateProduct创建商品admin
ListProducts商品列表(分页+多维筛选)公开
GetProduct获取商品详情公开
UpdateProduct更新商品信息admin
DeleteProduct删除商品admin
SyncProducts从 Provider 接口同步产品admin
BatchSetActive批量启用/禁用商品admin
BatchSetGroup批量设置商品分组admin
BatchDelete批量删除商品admin
ImportUpstreamProducts从上游导入商品admin
BatchUpdatePrice批量按比例更新价格admin

CreateProduct

描述

创建一个新商品。

Request — CreateProductRequest

字段类型编号必填描述
namestring1商品名称
descriptionstring2商品描述
price_per_monthstring9月度价格(字符串金额)
interface_idstring10关联的 Provider 接口 ID(UUID)
specsmap<string,string>12动态规格键值对
group_idstring?13商品分组 ID(UUID)
billing_cyclesmap<string,string>14多周期定价,如 {"monthly":"50.00","quarterly":"140.00"}
cost_pricestring15成本价(字符串金额)

Response — CreateProductResponse

字段类型编号描述
productProductInfo1创建的商品信息

错误码

错误说明
InvalidArgument参数校验失败
NotFound指定的 interface_id 不存在
PermissionDenied非 admin 用户

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"name":"VPS-1C2G","description":"1核2G VPS","price_per_month":"29.00","interface_id":"0192a123-...","specs":{"region":"us-west","cpu":"1","memory":"2"},"billing_cycles":{"monthly":"29.00","yearly":"290.00"}}' \
  localhost:50051 rustbill.product.ProductService/CreateProduct
const resp = await api.createProduct({
  name: "VPS-1C2G",
  description: "1核2G VPS",
  price_per_month: "29.00",
  interface_id: "0192a123-...",
  specs: { region: "us-west", cpu: "1", memory: "2" },
  billing_cycles: { monthly: "29.00", yearly: "290.00" }
});

ListProducts

描述

分页查询商品列表,支持按接口、地域、状态、关键词、分组、分类筛选。

Request — ListProductsRequest

字段类型编号必填描述
paginationPageRequest1分页参数
interface_idstring?2按 Provider 接口过滤
regionstring?3按地域过滤(匹配 specs.region
is_activebool?4按启用状态过滤
searchstring?5名称关键词搜索
group_idstring?6按分组过滤
category_idstring?8按分类过滤

Response — ListProductsResponse

字段类型编号描述
productsrepeated ProductInfo1商品列表
metaPageMeta2分页元数据

示例

grpcurl -plaintext \
  -d '{"pagination":{"page":1,"page_size":12},"region":"us-west","is_active":true}' \
  localhost:50051 rustbill.product.ProductService/ListProducts
const resp = await api.listProducts({
  pagination: { page: 1, page_size: 12 },
  region: "us-west",
  is_active: true
});

GetProduct

描述

获取单个商品详情。

Request — GetProductRequest

字段类型编号必填描述
idstring1商品 ID(UUID)

Response — GetProductResponse

字段类型编号描述
productProductInfo1商品信息

错误码

错误说明
NotFound商品不存在

示例

grpcurl -plaintext \
  -d '{"id":"0192a123-4567-7890-abcd-ef0123456789"}' \
  localhost:50051 rustbill.product.ProductService/GetProduct

UpdateProduct

描述

更新商品信息。所有字段均可选,仅更新传入的字段。

Request — UpdateProductRequest

字段类型编号必填描述
idstring1商品 ID
namestring?2商品名称
descriptionstring?3商品描述
price_per_monthstring?4月度价格
is_activebool?5启用状态
specsmap<string,string>6动态规格(完全替换)
group_idstring?7商品分组
billing_cyclesmap<string,string>8多周期定价(完全替换)
cost_pricestring?9成本价

Response — UpdateProductResponse

字段类型编号描述
productProductInfo1更新后的商品信息

错误码

错误说明
NotFound商品不存在
InvalidArgument参数校验失败
PermissionDenied非 admin 用户

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"id":"0192a123-...","price_per_month":"35.00","is_active":false}' \
  localhost:50051 rustbill.product.ProductService/UpdateProduct

DeleteProduct

描述

删除指定商品。

Request — DeleteProductRequest

字段类型编号必填描述
idstring1商品 ID(UUID)

Response — DeleteProductResponse

无字段。

错误码

错误说明
NotFound商品不存在
PermissionDenied非 admin 用户

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"id":"0192a123-4567-7890-abcd-ef0123456789"}' \
  localhost:50051 rustbill.product.ProductService/DeleteProduct

SyncProducts

描述

从指定 Provider 接口同步商品列表。调用 Provider 插件的 sync_products() 函数,返回上游商品并创建本地商品记录。

Request — SyncProductsRequest

字段类型编号必填描述
interface_idstring1Provider 接口 ID(UUID)
group_idstring2同步后归入的分组 ID

Response — SyncProductsResponse

字段类型编号描述
syncedrepeated ProductInfo1同步后的商品列表
countuint322同步数量

错误码

错误说明
NotFound接口不存在或已禁用
InternalProvider 插件执行失败
PermissionDenied非 admin 用户

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"interface_id":"0192a123-...","group_id":"0192b456-..."}' \
  localhost:50051 rustbill.product.ProductService/SyncProducts

BatchSetActive

描述

批量启用或禁用商品。

Request — BatchSetActiveRequest

字段类型编号必填描述
product_idsrepeated string1商品 ID 列表
is_activebool2目标启用状态

Response — BatchSetActiveResponse

字段类型编号描述
updated_countuint321实际更新的商品数量

错误码

错误说明
InvalidArgumentproduct_ids 为空
PermissionDenied非 admin 用户

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"product_ids":["id1","id2","id3"],"is_active":true}' \
  localhost:50051 rustbill.product.ProductService/BatchSetActive

BatchSetGroup

描述

批量将商品归入指定分组。group_id 为空字符串时取消分组。

Request — BatchSetGroupRequest

字段类型编号必填描述
product_idsrepeated string1商品 ID 列表
group_idstring2目标分组 ID(空字符串=取消分组)

Response — BatchSetGroupResponse

字段类型编号描述
updated_countuint321实际更新的商品数量

错误码

错误说明
InvalidArgumentproduct_ids 为空
PermissionDenied非 admin 用户

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"product_ids":["id1","id2"],"group_id":"0192b456-..."}' \
  localhost:50051 rustbill.product.ProductService/BatchSetGroup

BatchDelete

描述

批量删除商品。

Request — BatchDeleteRequest

字段类型编号必填描述
product_idsrepeated string1商品 ID 列表

Response — BatchDeleteResponse

字段类型编号描述
deleted_countuint321实际删除的商品数量

错误码

错误说明
InvalidArgumentproduct_ids 为空
PermissionDenied非 admin 用户

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"product_ids":["id1","id2"]}' \
  localhost:50051 rustbill.product.ProductService/BatchDelete

ImportUpstreamProducts

描述

将上游商品批量导入本地商品表。配合 IntegrationService/SyncProducts 从上游拉取数据后批量导入。

Request — ImportUpstreamProductsRequest

字段类型编号必填描述
interface_idstring1Provider 接口 ID
itemsrepeated UpstreamProductItem2待导入的商品列表
group_idstring3导入后归入的分组 ID

UpstreamProductItem

字段类型编号描述
namestring1商品名称
descriptionstring2商品描述
specsmap<string,string>3规格键值对
cost_pricestring4成本价

Response — ImportUpstreamProductsResponse

字段类型编号描述
imported_countuint321成功导入数量

错误码

错误说明
InvalidArgumentinterface_id 无效或 items 为空
PermissionDenied非 admin 用户

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"interface_id":"0192a123-...","items":[{"name":"VPS-1C1G","description":"1核1G","specs":{"cpu":"1"},"cost_price":"5.00"}],"group_id":"0192b456-..."}' \
  localhost:50051 rustbill.product.ProductService/ImportUpstreamProducts

BatchUpdatePrice

描述

批量按加价比例更新商品售价。新售价 = 成本价 × markup_ratio。

Request — BatchUpdatePriceRequest

字段类型编号必填描述
product_idsrepeated string1商品 ID 列表
markup_ratiostring2加价比例,如 "1.3" 表示 130%(即加价 30%)

Response — BatchUpdatePriceResponse

字段类型编号描述
updated_countuint321实际更新的商品数量

错误码

错误说明
InvalidArgumentmarkup_ratio 格式无效或 product_ids 为空
PermissionDenied非 admin 用户

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"product_ids":["id1","id2"],"markup_ratio":"1.3"}' \
  localhost:50051 rustbill.product.ProductService/BatchUpdatePrice

公共类型

ProductInfo

字段类型编号描述
idstring1商品 UUID
namestring2商品名称
descriptionstring3商品描述
price_per_monthstring10月度价格(字符串金额)
interface_idstring11关联的 Provider 接口 ID
is_activebool13是否启用
created_atstring14创建时间(RFC3339)
updated_atstring15更新时间(RFC3339)
specsmap<string,string>16动态规格键值对
group_idstring17商品分组 ID
billing_cyclesmap<string,string>18多周期定价映射
cost_pricestring19成本价(字符串金额)

SpecField

定义商品规格模板中的字段,由 Provider 插件提供。

字段类型编号描述
keystring1字段标识符
labelstring2显示标签
field_typestring3字段类型("text" / "number" / "select" / "radio"
requiredbool4是否必填
display_orderuint325显示顺序
optionsrepeated SpecOption6选项列表(select/radio 类型)
minint64?7最小值(number 类型)
maxint64?8最大值(number 类型)
default_valuestring?9默认值
unitstring10单位(如 "GB", "Mbps"
descriptionstring11字段描述
iconstring12图标名称
groupstring13所属分组 key
stepint64?14步长(number 类型)
price_impactPriceImpact15价格影响配置

SpecOption

字段类型编号描述
valuestring1选项值
labelstring2显示标签
iconstring3图标名称
descriptionstring4选项描述
price_modifierstring5价格修正值
disabledbool6是否禁用

SpecGroup

字段类型编号描述
keystring1分组标识符
labelstring2显示标签
iconstring3图标名称
display_orderuint324显示顺序
fieldsrepeated SpecField5包含的字段列表

PriceImpact

字段类型编号描述
modestring1计价模式("add" / "multiply"
amountstring2价格影响金额
currencystring3货币代码

CustomerService

客户管理服务,负责客户的 CRUD 操作。Customer 用户登录后自动限定数据范围为自身的 customer_id。

RPC 列表

RPC描述权限
CreateCustomer创建客户admin
ListCustomers客户列表(分页+筛选)admin
GetCustomer获取客户详情admin
UpdateCustomer更新客户信息admin
DeleteCustomer删除客户admin

CreateCustomer

描述

创建新客户。

Request — CreateCustomerRequest

字段类型编号必填描述
namestring1客户名称
contact_personstring2联系人
emailstring3联系邮箱
phonestring4联系电话
companystring5公司名称
notesstring6备注
credit_limitstring7信用额度(字符串金额)

Response — CreateCustomerResponse

字段类型编号描述
customerCustomerInfo1创建的客户信息

错误码

错误说明
InvalidArgument参数校验失败
AlreadyExists同名称客户已存在
PermissionDenied非 admin 用户

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"name":"ACME Corp","contact_person":"John Doe","email":"[email protected]","phone":"1234567890","company":"ACME Inc.","notes":"VIP客户","credit_limit":"10000.00"}' \
  localhost:50051 rustbill.customer.CustomerService/CreateCustomer
const resp = await api.createCustomer({
  name: "ACME Corp",
  contact_person: "John Doe",
  email: "[email protected]",
  phone: "1234567890",
  company: "ACME Inc.",
  notes: "VIP客户",
  credit_limit: "10000.00"
});

ListCustomers

描述

分页查询客户列表,支持按状态和关键词筛选。

Request — ListCustomersRequest

字段类型编号必填描述
paginationPageRequest1分页参数
is_activebool?2按启用状态过滤
searchstring?3名称/邮箱关键词搜索

Response — ListCustomersResponse

字段类型编号描述
customersrepeated CustomerInfo1客户列表
metaPageMeta2分页元数据

错误码

错误说明
PermissionDenied非 admin 用户

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"pagination":{"page":1,"page_size":20},"is_active":true,"search":"ACME"}' \
  localhost:50051 rustbill.customer.CustomerService/ListCustomers

GetCustomer

描述

获取单个客户详情。

Request — GetCustomerRequest

字段类型编号必填描述
idstring1客户 ID(UUID)

Response — GetCustomerResponse

字段类型编号描述
customerCustomerInfo1客户信息

错误码

错误说明
NotFound客户不存在
PermissionDenied非 admin 用户

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"id":"0192a123-4567-7890-abcd-ef0123456789"}' \
  localhost:50051 rustbill.customer.CustomerService/GetCustomer

UpdateCustomer

描述

更新客户信息。所有字段均可选,仅更新传入的字段。

Request — UpdateCustomerRequest

字段类型编号必填描述
idstring1客户 ID
namestring?2客户名称
contact_personstring?3联系人
emailstring?4联系邮箱
phonestring?5联系电话
companystring?6公司名称
notesstring?7备注
credit_limitstring?8信用额度
is_activebool?9启用状态
can_create_api_keysbool?10是否允许创建 API Key

Response — UpdateCustomerResponse

字段类型编号描述
customerCustomerInfo1更新后的客户信息

错误码

错误说明
NotFound客户不存在
InvalidArgument参数校验失败
PermissionDenied非 admin 用户

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"id":"0192a123-...","credit_limit":"20000.00","can_create_api_keys":true}' \
  localhost:50051 rustbill.customer.CustomerService/UpdateCustomer

DeleteCustomer

描述

删除指定客户。

Request — DeleteCustomerRequest

字段类型编号必填描述
idstring1客户 ID(UUID)

Response — DeleteCustomerResponse

无字段。

错误码

错误说明
NotFound客户不存在
PermissionDenied非 admin 用户

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"id":"0192a123-4567-7890-abcd-ef0123456789"}' \
  localhost:50051 rustbill.customer.CustomerService/DeleteCustomer

公共类型

CustomerInfo

字段类型编号描述
idstring1客户 UUID
namestring2客户名称
contact_personstring3联系人姓名
emailstring4联系邮箱
phonestring5联系电话
companystring6公司名称
notesstring7备注
balancestring8账户余额(字符串金额)
credit_limitstring9信用额度(字符串金额)
is_activebool10是否启用
created_atstring11创建时间(RFC3339)
updated_atstring12更新时间(RFC3339)
can_create_api_keysbool13是否允许创建 API Key

OrderService

订单全生命周期管理服务,涵盖下单、支付、查询和状态管理。订单状态从 pending 开始流转,支付成功后异步触发资源开通。

RPC 列表

RPC描述权限
CreateOrder创建订单(可选同步返回支付信息)已认证(customer)
ListOrders订单列表(分页+筛选)admin 全量 / customer 自身
GetOrder获取订单详情admin 全量 / customer 自身
UpdateOrder更新订单状态/备注admin
DeleteOrder删除订单admin
PayOrder发起支付已认证(customer)

CreateOrder

描述

创建新订单,可选择是否在创建时同时发起支付。

Request — CreateOrderRequest

字段类型编号必填描述
customer_idstring1客户 ID(UUID)
product_idstring2商品 ID(UUID)
provider_interface_idstring3Provider 接口 ID(UUID)
server_specServerSpec4服务器规格配置
currencystring5货币代码(如 "CNY"
gateway_interface_idstring6支付网关接口 ID(UUID)
notesstring7备注
billing_cyclestring8计费周期:"monthly" / "quarterly" / "yearly"

Response — CreateOrderResponse

字段类型编号描述
orderOrderInfo1创建的订单信息
paymentPaymentInfo?2支付信息(如果创建时请求支付)

错误码

错误说明
InvalidArgument参数校验失败(商品不存在、接口无效等)
NotFound商品、接口或客户不存在
Unauthenticated未登录

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"customer_id":"0192a123-...","product_id":"0192b456-...","provider_interface_id":"0192c789-...","server_spec":{"cpu_cores":2,"memory_gb":4,"disk_gb":50,"bandwidth_mbps":100,"region":"us-west","os":"ubuntu-22.04"},"currency":"CNY","gateway_interface_id":"0192d012-...","notes":"","billing_cycle":"monthly"}' \
  localhost:50051 rustbill.order.OrderService/CreateOrder
const resp = await api.createOrder({
  customer_id: "0192a123-...",
  product_id: "0192b456-...",
  provider_interface_id: "0192c789-...",
  server_spec: {
    cpu_cores: 2,
    memory_gb: 4,
    disk_gb: 50,
    bandwidth_mbps: 100,
    region: "us-west",
    os: "ubuntu-22.04"
  },
  currency: "CNY",
  gateway_interface_id: "0192d012-...",
  notes: "",
  billing_cycle: "monthly"
});

ListOrders

描述

分页查询订单列表。Admin 用户可查看所有订单,Customer 用户自动限定为自身订单。

Request — ListOrdersRequest

字段类型编号必填描述
paginationPageRequest1分页参数
customer_idstring?2按客户过滤(admin 可用)
statusstring?3按状态过滤
provider_interface_idstring?4按 Provider 接口过滤
searchstring?5关键词搜索

Response — ListOrdersResponse

字段类型编号描述
ordersrepeated OrderInfo1订单列表
metaPageMeta2分页元数据

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"pagination":{"page":1,"page_size":20},"status":"active"}' \
  localhost:50051 rustbill.order.OrderService/ListOrders

GetOrder

描述

获取单个订单详情。

Request — GetOrderRequest

字段类型编号必填描述
idstring1订单 ID(UUID)

Response — GetOrderResponse

字段类型编号描述
orderOrderInfo1订单信息

错误码

错误说明
NotFound订单不存在
PermissionDeniedcustomer 用户无权查看他人订单

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"id":"0192a123-4567-7890-abcd-ef0123456789"}' \
  localhost:50051 rustbill.order.OrderService/GetOrder

UpdateOrder

描述

更新订单状态、Provider 实例 ID 或备注。

Request — UpdateOrderRequest

字段类型编号必填描述
idstring1订单 ID
statusstring?2新状态
provider_instance_idstring?3上游实例 ID
notesstring?4备注

Response — UpdateOrderResponse

字段类型编号描述
orderOrderInfo1更新后的订单信息

错误码

错误说明
NotFound订单不存在
InvalidArgument状态流转不合法
PermissionDenied非 admin 用户

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"id":"0192a123-...","status":"active","notes":"已开通"}' \
  localhost:50051 rustbill.order.OrderService/UpdateOrder

DeleteOrder

描述

删除指定订单。

Request — DeleteOrderRequest

字段类型编号必填描述
idstring1订单 ID(UUID)

Response — DeleteOrderResponse

无字段。

错误码

错误说明
NotFound订单不存在
PermissionDenied非 admin 用户

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"id":"0192a123-4567-7890-abcd-ef0123456789"}' \
  localhost:50051 rustbill.order.OrderService/DeleteOrder

PayOrder

描述

对指定订单发起支付。调用 PaymentGateway 插件生成支付链接或二维码。支付成功后 event_worker 异步触发资源开通。

Request — PayOrderRequest

字段类型编号必填描述
order_idstring1订单 ID(UUID)
gateway_interface_idstring2支付网关接口 ID(UUID)

Response — PayOrderResponse

字段类型编号描述
paymentPaymentInfo1支付信息

PaymentInfo

字段类型编号描述
payment_idstring1支付记录 ID
gateway_interface_idstring2网关接口 ID
payment_urlstring3支付页面 URL
qr_codestring4二维码内容(Base64 或 URL)
instructionsstring5支付说明

错误码

错误说明
NotFound订单不存在
InvalidArgument订单状态不允许支付(如已支付或已取消)
Internal网关插件执行失败
Unauthenticated未登录

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"order_id":"0192a123-...","gateway_interface_id":"0192d012-..."}' \
  localhost:50051 rustbill.order.OrderService/PayOrder
const resp = await api.payOrder({
  order_id: "0192a123-...",
  gateway_interface_id: "0192d012-..."
});
// resp.payment.payment_url → 跳转到支付页面

公共类型

OrderInfo

字段类型编号描述
idstring1订单 UUID
customer_idstring2客户 ID
product_idstring3商品 ID
provider_interface_idstring4Provider 接口 ID
provider_instance_idstring5上游实例 ID(开通后填充)
server_specServerSpec6服务器规格
statusstring7订单状态
amountstring8订单金额(字符串)
currencystring9货币代码
gateway_interface_idstring10支付网关接口 ID
gateway_payment_idstring11网关支付 ID(支付后填充)
notesstring12备注
created_atstring13创建时间(RFC3339)
updated_atstring14更新时间(RFC3339)
product_namestring15商品名称(冗余)
billing_cyclestring16计费周期:"monthly" / "quarterly" / "yearly"

ServerSpec

字段类型编号描述
cpu_coresuint321CPU 核心数
memory_gbuint322内存(GB)
disk_gbuint323磁盘(GB)
bandwidth_mbpsuint324带宽(Mbps)
regionstring5地域
osstring6操作系统
extra_specsmap<string,string>7扩展规格键值对

订单状态流转

pending → paid → provisioning → active / suspended / cancelled / refunded / completed
                                      ↓
                                  terminated
状态说明
pending待支付
paid已支付(等待开通)
provisioning开通中
active运行中
suspended已暂停
cancelled已取消
refunded已退款
completed已完成

BillingService

账单管理服务,负责发票的 CRUD 操作和周期性账单生成。支持按计费周期自动汇总订单生成客户账单。

RPC 列表

RPC描述权限
CreateInvoice手动创建发票admin
ListInvoices发票列表(分页+筛选)admin 全量 / customer 自身
GetInvoice获取发票详情admin 全量 / customer 自身
UpdateInvoice更新发票状态/备注admin
DeleteInvoice删除发票admin
GenerateInvoices批量生成周期账单admin

CreateInvoice

描述

手动创建一张发票(含发票行项目)。

Request — CreateInvoiceRequest

字段类型编号必填描述
customer_idstring1客户 ID(UUID)
billing_period_startstring2计费周期开始(日期字符串)
billing_period_endstring3计费周期结束(日期字符串)
amountstring4小计金额(字符串)
tax_amountstring5税额(字符串)
total_amountstring6总金额(含税)
due_datestring7到期日期
notesstring8备注
itemsrepeated InvoiceItemInput9发票行项目列表

InvoiceItemInput

字段类型编号描述
descriptionstring1项目描述
quantityuint322数量
unit_pricestring3单价(字符串金额)
amountstring4金额(quantity × unit_price)

Response — CreateInvoiceResponse

字段类型编号描述
invoiceInvoiceInfo1创建的发票信息

错误码

错误说明
InvalidArgument参数校验失败
NotFound客户不存在
PermissionDenied非 admin 用户

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"customer_id":"0192a123-...","billing_period_start":"2024-01-01","billing_period_end":"2024-01-31","amount":"100.00","tax_amount":"13.00","total_amount":"113.00","due_date":"2024-02-15","notes":"1月账单","items":[{"description":"VPS-1C2G 月费","quantity":1,"unit_price":"100.00","amount":"100.00"}]}' \
  localhost:50051 rustbill.billing.BillingService/CreateInvoice
const resp = await api.createInvoice({
  customer_id: "0192a123-...",
  billing_period_start: "2024-01-01",
  billing_period_end: "2024-01-31",
  amount: "100.00",
  tax_amount: "13.00",
  total_amount: "113.00",
  due_date: "2024-02-15",
  notes: "1月账单",
  items: [{ description: "VPS-1C2G 月费", quantity: 1, unit_price: "100.00", amount: "100.00" }]
});

ListInvoices

描述

分页查询发票列表。Admin 可查看所有发票,Customer 自动限定为自身发票。

Request — ListInvoicesRequest

字段类型编号必填描述
paginationPageRequest1分页参数
customer_idstring?2按客户过滤(admin 可用)
statusstring?3按状态过滤

Response — ListInvoicesResponse

字段类型编号描述
invoicesrepeated InvoiceInfo1发票列表
metaPageMeta2分页元数据

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"pagination":{"page":1,"page_size":20},"status":"issued"}' \
  localhost:50051 rustbill.billing.BillingService/ListInvoices

GetInvoice

描述

获取单张发票详情,含发票行项目。

Request — GetInvoiceRequest

字段类型编号必填描述
idstring1发票 ID(UUID)

Response — GetInvoiceResponse

字段类型编号描述
invoiceInvoiceInfo1发票信息

错误码

错误说明
NotFound发票不存在
PermissionDeniedcustomer 用户无权查看他人发票

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"id":"0192a123-4567-7890-abcd-ef0123456789"}' \
  localhost:50051 rustbill.billing.BillingService/GetInvoice

UpdateInvoice

描述

更新发票状态或备注。

Request — UpdateInvoiceRequest

字段类型编号必填描述
idstring1发票 ID
statusstring?2新状态
notesstring?3备注

Response — UpdateInvoiceResponse

字段类型编号描述
invoiceInvoiceInfo1更新后的发票信息

错误码

错误说明
NotFound发票不存在
InvalidArgument状态流转不合法
PermissionDenied非 admin 用户

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"id":"0192a123-...","status":"paid"}' \
  localhost:50051 rustbill.billing.BillingService/UpdateInvoice

DeleteInvoice

描述

删除指定发票。

Request — DeleteInvoiceRequest

字段类型编号必填描述
idstring1发票 ID(UUID)

Response — DeleteInvoiceResponse

无字段。

错误码

错误说明
NotFound发票不存在
PermissionDenied非 admin 用户

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"id":"0192a123-4567-7890-abcd-ef0123456789"}' \
  localhost:50051 rustbill.billing.BillingService/DeleteInvoice

GenerateInvoices

描述

按指定计费周期批量生成客户发票。系统遍历周期内所有已完成或活跃的订单,按客户汇总生成发票。

Request — GenerateInvoicesRequest

字段类型编号必填描述
billing_period_startstring1计费周期开始(日期字符串)
billing_period_endstring2计费周期结束(日期字符串)

Response — GenerateInvoicesResponse

字段类型编号描述
generated_countuint321生成的发票数量

错误码

错误说明
InvalidArgument日期格式无效或周期非法
PermissionDenied非 admin 用户

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"billing_period_start":"2024-01-01","billing_period_end":"2024-01-31"}' \
  localhost:50051 rustbill.billing.BillingService/GenerateInvoices

公共类型

InvoiceInfo

字段类型编号描述
idstring1发票 UUID
invoice_numberstring2发票编号
customer_idstring3客户 ID
billing_period_startstring4计费周期开始
billing_period_endstring5计费周期结束
amountstring6小计金额(字符串)
tax_amountstring7税额(字符串)
total_amountstring8总金额(含税,字符串)
statusstring9发票状态
issued_atstring10签发时间(RFC3339)
paid_atstring11支付时间(RFC3339)
due_datestring12到期日期
notesstring13备注
created_atstring14创建时间(RFC3339)
updated_atstring15更新时间(RFC3339)
itemsrepeated InvoiceItemInfo16发票行项目

InvoiceItemInfo

字段类型编号描述
idstring1行项目 UUID
descriptionstring2项目描述
quantityuint323数量
unit_pricestring4单价(字符串金额)
amountstring5金额(quantity × unit_price)

发票状态流转

draft → issued → paid / overdue → cancelled
状态说明
draft草稿
issued已签发(待支付)
paid已支付
overdue已逾期
cancelled已取消

PaymentService

支付管理服务,负责支付创建、状态查询、网关回调处理和退款操作。支持对接多种 PaymentGateway 插件(如易支付、银行转账)。

RPC 列表

RPC描述权限
CreatePayment创建支付记录(生成支付链接)已认证
ListPayments支付列表(分页+筛选)admin 全量 / customer 自身
GetPayment获取支付详情admin 全量 / customer 自身
QueryPaymentStatus向网关查询支付状态admin
HandleCallback接收支付网关异步回调公开(网关调用)
RefundPayment发起退款admin

CreatePayment

描述

为订单或发票创建支付记录。调用 PaymentGateway 插件生成支付 URL 或二维码。

Request — CreatePaymentRequest

字段类型编号必填描述
order_idstring1订单 ID(UUID)
invoice_idstring2发票 ID(UUID)
gateway_interface_idstring3支付网关接口 ID(UUID)
return_urlstring4支付完成后跳转 URL

Response — CreatePaymentResponse

字段类型编号描述
paymentPaymentRecord1支付记录
payment_urlstring2支付页面 URL
qr_codestring3二维码内容(Base64 或 URL)
instructionsstring4支付说明

错误码

错误说明
InvalidArgument订单/发票不存在或已支付
Internal网关插件执行失败
Unauthenticated未登录

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"order_id":"0192a123-...","invoice_id":"","gateway_interface_id":"0192d012-...","return_url":"https://example.com/payment/result"}' \
  localhost:50051 rustbill.payment.PaymentService/CreatePayment
const resp = await api.createPayment({
  order_id: "0192a123-...",
  invoice_id: "",
  gateway_interface_id: "0192d012-...",
  return_url: "https://example.com/payment/result"
});
// 跳转到 resp.payment_url

ListPayments

描述

分页查询支付记录列表。Admin 可查看所有支付,Customer 自动限定为自身支付。

Request — ListPaymentsRequest

字段类型编号必填描述
paginationPageRequest1分页参数
order_idstring?2按订单过滤
invoice_idstring?3按发票过滤
gateway_interface_idstring?4按网关接口过滤
statusstring?5按状态过滤

Response — ListPaymentsResponse

字段类型编号描述
paymentsrepeated PaymentRecord1支付列表
metaPageMeta2分页元数据

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"pagination":{"page":1,"page_size":20},"status":"success"}' \
  localhost:50051 rustbill.payment.PaymentService/ListPayments

GetPayment

描述

获取单条支付记录详情。

Request — GetPaymentRequest

字段类型编号必填描述
idstring1支付记录 ID(UUID)

Response — GetPaymentResponse

字段类型编号描述
paymentPaymentRecord1支付记录

错误码

错误说明
NotFound支付记录不存在
PermissionDeniedcustomer 用户无权查看他人支付

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"id":"0192a123-4567-7890-abcd-ef0123456789"}' \
  localhost:50051 rustbill.payment.PaymentService/GetPayment

QueryPaymentStatus

描述

主动向支付网关查询支付状态并同步本地记录。

Request — QueryPaymentStatusRequest

字段类型编号必填描述
payment_idstring1支付记录 ID(UUID)

Response — QueryPaymentStatusResponse

字段类型编号描述
paymentPaymentRecord1更新后的支付记录

错误码

错误说明
NotFound支付记录不存在
Internal网关查询失败
PermissionDenied非 admin 用户

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"payment_id":"0192a123-..."}' \
  localhost:50051 rustbill.payment.PaymentService/QueryPaymentStatus

HandleCallback

描述

接收支付网关的异步回调通知。使用乐观锁(expected_status)确保回调幂等,防止重复处理。

注意:此 RPC 由支付网关调用,由前端直接调用。

Request — PaymentCallbackRequest

字段类型编号必填描述
gateway_interface_idstring1网关接口 ID(UUID)
payloadstring2网关回调原始 payload(由网关插件自行解析)

Response — PaymentCallbackResponse

字段类型编号描述
paymentPaymentRecord1更新后的支付记录

错误码

错误说明
InvalidArgument签名验证失败、订单号不匹配
NotFound支付记录不存在
AlreadyExists重复回调(乐观锁拦截)

回调流程

支付网关 → POST /rustbill.payment.PaymentService/HandleCallback
  → gateway 插件验证签名
  → 更新 payment.status = "success"
  → 更新 order.status = "paid"
  → INSERT event_queue(order_paid)  ← event_worker 异步处理开通

RefundPayment

描述

对已支付订单发起退款。amount 为空时全额退款。

Request — RefundPaymentRequest

字段类型编号必填描述
payment_idstring1支付记录 ID(UUID)
amountstring2退款金额(空字符串=全额退款)

Response — RefundPaymentResponse

字段类型编号描述
paymentPaymentRecord1更新后的支付记录

错误码

错误说明
NotFound支付记录不存在
InvalidArgument退款金额超过支付金额、支付状态不允许退款
Internal网关退款执行失败
PermissionDenied非 admin 用户

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"payment_id":"0192a123-...","amount":""}' \
  localhost:50051 rustbill.payment.PaymentService/RefundPayment

公共类型

PaymentRecord

字段类型编号描述
idstring1支付记录 UUID
order_idstring2关联订单 ID
invoice_idstring3关联发票 ID
amountstring4支付金额(字符串)
currencystring5货币代码
gateway_interface_idstring6支付网关接口 ID
gateway_payment_idstring7网关内支付 ID(回调后填充)
gateway_tx_idstring8网关交易流水号
statusstring9支付状态
paid_atstring10支付时间(RFC3339)
created_atstring11创建时间(RFC3339)
updated_atstring12更新时间(RFC3339)

支付状态流转

pending → success / failed / refunded
状态说明
pending待支付
success支付成功
failed支付失败
refunded已退款

IntegrationService

插件系统与供应商集成的核心服务,负责插件定义管理、接口实例 CRUD、供应商询价/下单、上游实例同步和余额查询。是 Admin UI 插件管理页和 Upstream 页的主要后端服务。

RPC 列表

RPC描述权限
ListProviders列出所有 Provider 插件接口admin
ListGateways列出所有 Gateway 插件接口admin
ListNotifiers列出所有 Notifier 插件接口admin
ListPlugins列出所有插件定义(.rn 文件)admin
GetPlugin获取单个插件定义admin
ListPluginInterfaces列出所有插件接口实例admin
GetPluginInterface获取单个接口实例详情admin
CreatePluginInterface创建新的接口实例admin
UpdatePluginInterface更新接口实例配置admin
TogglePluginInterface启用/禁用接口实例admin
DeletePluginInterface删除接口实例admin
CheckPluginHealth检查接口实例健康状态admin
GetPluginAdminPage获取插件管理嵌入页面 HTMLadmin
CallPluginAction调用插件自定义 action(admin panel bridge)admin
RestartServer重启服务器admin
SyncProducts从 Provider 接口同步商品已认证
QueryPrice向 Provider 询价已认证
CreateUpstreamInstance向上游 Provider 下单开通实例已认证
GetInstanceStatus查询上游实例状态admin
SyncUpstreamInstances从上游同步实例列表admin
ImportUpstreamInstance导入上游实例到本地admin
GetUpstreamBalance查询上游账户余额admin

ListProviders

描述

列出所有已启用的 Provider 类型插件接口实例,含健康状态和规格模板。

Request — EmptyRequest

无字段。

Response — ListProvidersResponse

字段类型编号描述
providersrepeated ProviderInfo1Provider 列表

ProviderInfo

字段类型编号描述
interface_idstring1接口实例 ID
provider_namestring2提供者名称
provider_typestring3类型:"upstream" / "first_party"
is_healthybool4是否健康
spec_templateSpecTemplate5规格模板(含字段定义和分组)

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{}' \
  localhost:50051 rustbill.integration.IntegrationService/ListProviders

ListGateways

描述

列出所有已启用的 Gateway 类型插件接口实例。

Request — EmptyRequest

无字段。

Response — ListGatewaysResponse

字段类型编号描述
gatewaysrepeated GatewayInfo1Gateway 列表

GatewayInfo

字段类型编号描述
gateway_idstring1接口实例 ID
gateway_namestring2网关名称
is_healthybool3是否健康

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{}' \
  localhost:50051 rustbill.integration.IntegrationService/ListGateways

ListNotifiers

描述

列出所有已启用的 Notifier 类型插件接口实例。

Request — EmptyRequest

无字段。

Response — ListNotifiersResponse

字段类型编号描述
notifiersrepeated NotifierInfo1Notifier 列表

NotifierInfo

字段类型编号描述
channel_idstring1接口实例 ID
channel_namestring2渠道名称
is_healthybool3是否健康

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{}' \
  localhost:50051 rustbill.integration.IntegrationService/ListNotifiers

ListPlugins

描述

列出所有已扫描的插件定义(plugins 表记录,由 PluginScanner 自动同步自 plugins/*.rn 文件)。

Request — ListPluginsRequest

字段类型编号必填描述
plugin_typestring?1按类型过滤:"first_party_provider" / "upstream_provider" / "gateway" / "notifier"

Response — ListPluginsResponse

字段类型编号描述
pluginsrepeated PluginDef1插件定义列表

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"plugin_type":"gateway"}' \
  localhost:50051 rustbill.integration.IntegrationService/ListPlugins

GetPlugin

描述

获取单个插件定义详情。

Request — GetPluginRequest

字段类型编号必填描述
idstring1插件定义 ID(UUID)

Response — GetPluginResponse

字段类型编号描述
pluginPluginDef1插件定义

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"id":"0192a123-..."}' \
  localhost:50051 rustbill.integration.IntegrationService/GetPlugin

ListPluginInterfaces

描述

列出所有插件接口实例,可按类型过滤。

Request — ListPluginInterfacesRequest

字段类型编号必填描述
plugin_typestring?1按类型过滤

Response — ListPluginInterfacesResponse

字段类型编号描述
interfacesrepeated PluginInterfaceInfo1接口实例列表

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"plugin_type":"first_party_provider"}' \
  localhost:50051 rustbill.integration.IntegrationService/ListPluginInterfaces

GetPluginInterface

描述

获取单个接口实例详情。

Request — GetPluginInterfaceRequest

字段类型编号必填描述
interface_idstring1接口实例 ID(UUID)

Response — GetPluginInterfaceResponse

字段类型编号描述
interfacePluginInterfaceInfo1接口实例详情

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"interface_id":"0192a123-..."}' \
  localhost:50051 rustbill.integration.IntegrationService/GetPluginInterface

CreatePluginInterface

描述

基于插件定义创建新的接口实例。服务端自动从插件脚本提取 config_schema() 填充初始 config_schema 字段。

Request — CreatePluginInterfaceRequest

字段类型编号必填描述
plugin_def_idstring1插件定义 ID(UUID)
display_namestring2显示名称(同一插件下唯一)
config_jsonstring3初始配置 JSON 字符串

Response — CreatePluginInterfaceResponse

字段类型编号描述
interfacePluginInterfaceInfo1创建的接口实例

错误码

错误说明
NotFound插件定义不存在
AlreadyExists同插件下 display_name 重复
InvalidArgumentconfig_json 格式无效
PermissionDenied非 admin 用户

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"plugin_def_id":"0192a123-...","display_name":"易支付-生产","config_json":"{\"api_url\":\"https://pay.example.com\",\"key\":\"xxx\"}"}' \
  localhost:50051 rustbill.integration.IntegrationService/CreatePluginInterface

UpdatePluginInterface

描述

更新接口实例的配置或显示名称。更新 config_json 后自动 evict 脚本缓存,下次调用热加载。

Request — UpdatePluginInterfaceRequest

字段类型编号必填描述
interface_idstring1接口实例 ID(UUID)
config_jsonstring?2新配置 JSON
display_namestring?3新显示名称

Response — UpdatePluginInterfaceResponse

字段类型编号描述
interfacePluginInterfaceInfo1更新后的接口实例

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"interface_id":"0192a123-...","config_json":"{\"api_url\":\"https://pay2.example.com\",\"key\":\"yyy\"}"}' \
  localhost:50051 rustbill.integration.IntegrationService/UpdatePluginInterface

TogglePluginInterface

描述

启用或禁用接口实例。

Request — TogglePluginInterfaceRequest

字段类型编号必填描述
interface_idstring1接口实例 ID(UUID)
enablebool2true=启用, false=禁用

Response — TogglePluginInterfaceResponse

字段类型编号描述
interfacePluginInterfaceInfo1更新后的接口实例

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"interface_id":"0192a123-...","enable":true}' \
  localhost:50051 rustbill.integration.IntegrationService/TogglePluginInterface

DeletePluginInterface

描述

删除接口实例。

Request — DeletePluginInterfaceRequest

字段类型编号必填描述
interface_idstring1接口实例 ID(UUID)

Response — DeletePluginInterfaceResponse

无字段。

错误码

错误说明
NotFound接口实例不存在
PermissionDenied非 admin 用户

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"interface_id":"0192a123-..."}' \
  localhost:50051 rustbill.integration.IntegrationService/DeletePluginInterface

CheckPluginHealth

描述

检查指定接口实例的健康状态。调用插件脚本的 health_check() 函数。

Request — CheckPluginHealthRequest

字段类型编号必填描述
interface_idstring1接口实例 ID(UUID)

Response — CheckPluginHealthResponse

字段类型编号描述
is_healthybool1是否健康
errorstring?2错误信息(不健康时)
checked_atstring3检查时间(RFC3339)

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"interface_id":"0192a123-..."}' \
  localhost:50051 rustbill.integration.IntegrationService/CheckPluginHealth

GetPluginAdminPage

描述

获取插件自定义管理页面的 HTML 内容,在 Admin UI 中以 iframe 嵌入渲染。

Request — GetPluginAdminPageRequest

字段类型编号必填描述
interface_idstring1接口实例 ID(UUID)

Response — GetPluginAdminPageResponse

字段类型编号描述
html_contentstring1HTML 页面内容
titlestring2页面标题

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"interface_id":"0192a123-..."}' \
  localhost:50051 rustbill.integration.IntegrationService/GetPluginAdminPage

CallPluginAction

描述

在插件管理页面中调用插件的自定义 action 函数(通过 postMessage bridge)。function 名称为插件脚本中的函数名,args_json 自动合并到 config 中传入。

Request — CallPluginActionRequest

字段类型编号必填描述
interface_idstring1接口实例 ID(UUID)
functionstring2要调用的函数名(如 "admin_list_instances"
args_jsonstring3JSON 编码的参数(合并入 config)

Response — CallPluginActionResponse

字段类型编号描述
result_jsonstring1JSON 编码的返回值

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"interface_id":"0192a123-...","function":"admin_list_instances","args_json":"{}"}' \
  localhost:50051 rustbill.integration.IntegrationService/CallPluginAction

RestartServer

描述

重启 RustBill 服务器(执行 graceful shutdown 后由 systemd/进程管理器重新拉起)。

Request — EmptyRequest

无字段。

Response — EmptyResponse

无字段。

错误码

错误说明
PermissionDenied非 admin 用户

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{}' \
  localhost:50051 rustbill.integration.IntegrationService/RestartServer

SyncProducts

描述

从指定的 Provider 接口同步商品列表到本地。调用 Provider 插件的 sync_products() 函数。

详情同 ProductService 中的 SyncProducts。

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"interface_id":"0192a123-..."}' \
  localhost:50051 rustbill.integration.IntegrationService/SyncProducts

QueryPrice

描述

向 Provider 插件查询指定规格的价格。调用 Provider 插件的 query_price() 函数。

Request — QueryPriceRequest

字段类型编号必填描述
interface_idstring1Provider 接口 ID(UUID)
cpu_coresuint322CPU 核心数
memory_gbuint323内存(GB)
disk_gbuint324磁盘(GB)
bandwidth_mbpsuint325带宽(Mbps)
regionstring6地域
osstring7操作系统
extra_specsmap<string,string>8扩展规格

Response — QueryPriceResponse

字段类型编号描述
amountstring1价格(字符串金额)
currencystring2货币代码
billing_cyclestring3计费周期

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"interface_id":"0192a123-...","cpu_cores":2,"memory_gb":4,"disk_gb":50,"bandwidth_mbps":100,"region":"us-west","os":"ubuntu-22.04"}' \
  localhost:50051 rustbill.integration.IntegrationService/QueryPrice

CreateUpstreamInstance

描述

向上游 Provider 下单创建实例。调用 Provider 插件的 create_instance() 函数。

Request — UpstreamOrderRequest

字段类型编号必填描述
interface_idstring1Provider 接口 ID(UUID)
server_specServerSpec2服务器规格(引用 rustbill.order.ServerSpec

Response — UpstreamOrderResponse

字段类型编号描述
provider_instance_idstring1上游实例 ID
statusstring2实例状态
ip_addressstring3IP 地址

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"interface_id":"0192a123-...","server_spec":{"cpu_cores":2,"memory_gb":4,"disk_gb":50,"bandwidth_mbps":100,"region":"us-west","os":"ubuntu-22.04"}}' \
  localhost:50051 rustbill.integration.IntegrationService/CreateUpstreamInstance

GetInstanceStatus

描述

查询上游 Provider 中指定实例的状态。

Request — InstanceStatusRequest

字段类型编号必填描述
interface_idstring1Provider 接口 ID(UUID)
instance_idstring2上游实例 ID

Response — InstanceStatusResponse

字段类型编号描述
statusstring1实例状态
ip_addressstring2IP 地址

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"interface_id":"0192a123-...","instance_id":"i-abc123"}' \
  localhost:50051 rustbill.integration.IntegrationService/GetInstanceStatus

SyncUpstreamInstances

描述

从上游 Provider 同步实例列表。调用 Provider 插件的 sync_upstream_instances() 函数。

Request — SyncUpstreamInstancesRequest

字段类型编号必填描述
interface_idstring1Provider 接口 ID(UUID)

Response — SyncUpstreamInstancesResponse

字段类型编号描述
instancesrepeated UpstreamInstanceItem1上游实例列表

UpstreamInstanceItem

字段类型编号描述
instance_idstring1上游实例 ID
statusstring2实例状态
ip_addressstring3IP 地址
created_atstring4创建时间

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"interface_id":"0192a123-..."}' \
  localhost:50051 rustbill.integration.IntegrationService/SyncUpstreamInstances

ImportUpstreamInstance

描述

将上游实例导入本地 instances 表,建立关联。

Request — ImportUpstreamInstanceRequest

字段类型编号必填描述
interface_idstring1Provider 接口 ID(UUID)
instance_idstring2上游实例 ID

Response — ImportUpstreamInstanceResponse

字段类型编号描述
instance_idstring1本地实例 ID
statusstring2实例状态
ip_addressstring3IP 地址

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"interface_id":"0192a123-...","instance_id":"i-abc123"}' \
  localhost:50051 rustbill.integration.IntegrationService/ImportUpstreamInstance

GetUpstreamBalance

描述

查询上游 Provider 的账户余额。调用 Provider 插件的 get_upstream_balance() 函数。

Request — GetUpstreamBalanceRequest

字段类型编号必填描述
interface_idstring1Provider 接口 ID(UUID)

Response — GetUpstreamBalanceResponse

字段类型编号描述
balancestring1余额(字符串金额)
currencystring2货币代码

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"interface_id":"0192a123-..."}' \
  localhost:50051 rustbill.integration.IntegrationService/GetUpstreamBalance

公共类型

PluginDef

插件定义(plugins 表记录,由 PluginScanner 从 plugins/*.rn 文件扫描同步)。

字段类型编号描述
idstring1插件定义 UUID
plugin_typestring2类型:"first_party_provider" / "upstream_provider" / "gateway" / "notifier"
plugin_idstring3插件标识符(如 "incus", "yipay", "webhook"
has_admin_pagebool4是否有自定义管理页面(预计算列)
created_atstring5创建时间(RFC3339)
updated_atstring6更新时间(RFC3339)
versionstring7插件版本号(semver,来自 fn version(),默认 "0.0.0"

PluginInterfaceInfo

接口实例(plugin_interfaces 表记录,管理员通过 UI 创建和管理)。

字段类型编号描述
idstring1接口实例 UUID
plugin_def_idstring2关联插件定义 ID(FK)
plugin_typestring3类型(反范式列)
plugin_idstring4插件标识符(反范式列)
display_namestring5显示名称
enabledbool6是否启用
config_jsonstring7配置 JSON 字符串
config_schemastring8配置 JSON Schema(用于 UI 表单渲染)
created_atstring9创建时间(RFC3339)
updated_atstring10更新时间(RFC3339)
is_healthybool11是否健康
health_errorstring?12健康检查错误信息
last_check_atstring?13最近一次健康检查时间
has_admin_pagebool14是否有管理页面
plugin_versionstring15插件版本快照(创建时从父插件复制)

插件类型说明

类型DB 值说明
FirstPartyProviderfirst_party_provider自建资源(KVM, Incus)
UpstreamProviderupstream_provider上游分销(RustBill 上游)
PaymentGatewaygateway支付网关(易支付, 银行转账)
Notifiernotifier通知渠道(Webhook, Email)

NotificationService

通知管理服务,负责列出已启用的通知渠道和手动发送测试通知。系统事件(订单支付、开通完成、工单分配等)自动广播到所有启用的通知渠道。

RPC 列表

RPC描述权限
ListNotifiers列出所有通知渠道(含支持的事件类型)admin
SendNotification手动发送测试通知admin

ListNotifiers

描述

列出所有已启用的 Notifier 类型插件接口实例,含健康状态和支持的事件类型。

Request — EmptyRequest

无字段。

Response — ListNotifiersResponse

字段类型编号描述
notifiersrepeated NotifierInfo1通知渠道列表

NotifierInfo

字段类型编号描述
channel_idstring1接口实例 ID(UUID)
channel_namestring2渠道显示名称
supported_eventsrepeated string3支持的事件类型列表(如 ["order_paid","order_provisioned"]
is_healthybool4是否健康

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{}' \
  localhost:50051 rustbill.notification.NotificationService/ListNotifiers
const resp = await api.listNotifiers({});
// resp.notifiers → [{ channel_id, channel_name, supported_events, is_healthy }]

SendNotification

描述

手动发送测试通知到指定渠道或全部渠道。选择部分渠道时在其他客户端注册的回调等。

系统事件通知由服务端自动广播,无需手动调用此 RPC。

Request — SendNotificationRequest

字段类型编号必填描述
event_typestring1事件类型(如 "order_paid", "test"
titlestring2通知标题
bodystring3通知正文
recipientstring4接收者(如邮箱地址,为空时使用渠道默认接收者)
metadatamap<string,string>5附加元数据
channel_idsrepeated string6目标渠道 ID 列表(空=全部渠道)

Response — SendNotificationResponse

字段类型编号描述
resultsrepeated NotificationResult1各渠道发送结果

NotificationResult

字段类型编号描述
channel_idstring1渠道 ID
successbool2是否发送成功
error_messagestring3错误信息(失败时)

错误码

错误说明
InvalidArgumentevent_type 或 title 为空
PermissionDenied非 admin 用户

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"event_type":"test","title":"测试通知","body":"这是一条测试消息","recipient":"[email protected]","metadata":{},"channel_ids":[]}' \
  localhost:50051 rustbill.notification.NotificationService/SendNotification
const resp = await api.sendNotification({
  event_type: "test",
  title: "测试通知",
  body: "这是一条测试消息",
  recipient: "[email protected]",
  metadata: {},
  channel_ids: []
});
// resp.results → [{ channel_id, success, error_message }]

系统事件通知

以下事件会触发系统自动广播通知到所有已启用的通知渠道:

事件类型触发时机接收者
order_paid订单支付成功客户邮箱 + 管理员通知邮箱
order_provisioned资源开通完成客户邮箱
order_cancelled订单取消(含退款)客户邮箱
ticket_assigned工单分配处理人新旧处理人
ticket_replied工单有新的公开回复工单创建者

TicketService

工单管理服务,负责客户支持工单的全生命周期管理。支持创建工单、状态流转、处理人分配和回复(含内部备注)。Admin 用户可管理所有工单,Customer 用户只能查看和操作自己的工单。

RPC 列表

RPC描述权限
CreateTicket创建工单已认证
ListTickets工单列表(分页+筛选)admin 全量 / customer 自身
GetTicket获取工单详情(含回复列表)admin 全量 / customer 自身
UpdateTicket更新工单状态/优先级/处理人/描述admin
CreateReply添加工单回复已认证
DeleteTicket删除工单admin

CreateTicket

描述

创建新工单。Admin 创建时可指定 customer_id 为特定客户创建;Customer 用户创建时自动绑定自身客户。

Request — CreateTicketRequest

字段类型编号必填描述
customer_idstring1客户 ID(admin 指定,customer 自动设置)
titlestring2工单标题
descriptionstring3问题描述
prioritystring4优先级:"low" / "medium" / "high" / "urgent"(默认 "medium"

Response — CreateTicketResponse

字段类型编号描述
ticketTicketInfo1创建的工单信息

错误码

错误说明
InvalidArgument标题为空或优先级无效
NotFound指定客户不存在
Unauthenticated未登录

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"customer_id":"0192a123-...","title":"服务器无法连接","description":"VPS 无法通过 SSH 连接,已尝试重启无效","priority":"high"}' \
  localhost:50051 rustbill.ticket.TicketService/CreateTicket
const resp = await api.createTicket({
  customer_id: "0192a123-...",
  title: "服务器无法连接",
  description: "VPS 无法通过 SSH 连接,已尝试重启无效",
  priority: "high"
});

ListTickets

描述

分页查询工单列表。Admin 可查看所有工单,Customer 自动限定为自身工单。

Request — ListTicketsRequest

字段类型编号必填描述
paginationPageRequest1分页参数
customer_idstring?2按客户过滤(admin 可用)
statusstring?3按状态过滤:"pending" / "processing" / "resolved" / "closed"
prioritystring?4按优先级过滤:"low" / "medium" / "high" / "urgent"
assignee_user_idstring?5按处理人过滤

Response — ListTicketsResponse

字段类型编号描述
ticketsrepeated TicketInfo1工单列表
metaPageMeta2分页元数据

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"pagination":{"page":1,"page_size":20},"status":"pending","priority":"high"}' \
  localhost:50051 rustbill.ticket.TicketService/ListTickets

GetTicket

描述

获取单个工单详情,包含完整回复列表。is_internal=true 的回复仅对 admin 可见,customer 请求中自动过滤。

Request — GetTicketRequest

字段类型编号必填描述
idstring1工单 ID(UUID)

Response — GetTicketResponse

字段类型编号描述
ticketTicketInfo1工单信息(含回复列表)

错误码

错误说明
NotFound工单不存在
PermissionDeniedcustomer 用户无权查看他人工单

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"id":"0192a123-4567-7890-abcd-ef0123456789"}' \
  localhost:50051 rustbill.ticket.TicketService/GetTicket

UpdateTicket

描述

更新工单状态、优先级、处理人或描述。更改处理人时会触发通知系统向新旧处理人发送工单分配通知。

Request — UpdateTicketRequest

字段类型编号必填描述
idstring1工单 ID
statusstring?2新状态:"pending" / "processing" / "resolved" / "closed"
prioritystring?3新优先级
assignee_user_idstring?4新处理人用户 ID(变更时触发通知)
descriptionstring?5更新的问题描述

Response — UpdateTicketResponse

字段类型编号描述
ticketTicketInfo1更新后的工单信息

错误码

错误说明
NotFound工单不存在
InvalidArgument状态流转不合法
PermissionDenied非 admin 用户

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"id":"0192a123-...","status":"processing","assignee_user_id":"0192b456-..."}' \
  localhost:50051 rustbill.ticket.TicketService/UpdateTicket

CreateReply

描述

添加工单回复。is_internal=true 的回复为内部备注,仅 admin 可见。

Request — CreateReplyRequest

字段类型编号必填描述
ticket_idstring1工单 ID(UUID)
contentstring2回复内容
is_internalbool3是否为内部备注

Response — CreateReplyResponse

字段类型编号描述
replyTicketReplyInfo1创建的回复信息

错误码

错误说明
NotFound工单不存在
InvalidArgument回复内容为空
Unauthenticated未登录

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"ticket_id":"0192a123-...","content":"已定位问题,正在修复","is_internal":false}' \
  localhost:50051 rustbill.ticket.TicketService/CreateReply
const resp = await api.createReply({
  ticket_id: "0192a123-...",
  content: "请提供更多信息",
  is_internal: false
});

DeleteTicket

描述

删除指定工单及其所有回复。

Request — DeleteTicketRequest

字段类型编号必填描述
idstring1工单 ID(UUID)

Response — DeleteTicketResponse

无字段。

错误码

错误说明
NotFound工单不存在
PermissionDenied非 admin 用户

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"id":"0192a123-4567-7890-abcd-ef0123456789"}' \
  localhost:50051 rustbill.ticket.TicketService/DeleteTicket

公共类型

TicketInfo

字段类型编号描述
idstring1工单 UUID
customer_idstring2关联客户 ID
titlestring3工单标题
descriptionstring4问题描述
statusstring5状态:"pending" / "processing" / "resolved" / "closed"
prioritystring6优先级:"low" / "medium" / "high" / "urgent"
assignee_user_idstring7处理人用户 ID
creator_user_idstring8创建人用户 ID(admin 创建时设置)
created_atstring9创建时间(RFC3339)
updated_atstring10更新时间(RFC3339)
repliesrepeated TicketReplyInfo11回复列表

TicketReplyInfo

字段类型编号描述
idstring1回复 UUID
ticket_idstring2关联工单 ID
user_idstring3回复人 admin 用户 ID(customer 回复时为空)
customer_idstring4回复人客户 ID(admin 回复时为空)
contentstring5回复内容
is_internalbool6是否为内部备注(customer 不可见)
created_atstring7创建时间(RFC3339)

工单状态流转

pending → processing → resolved → closed
状态说明
pending待处理
processing处理中
resolved已解决
closed已关闭

工单优先级

优先级说明
low非紧急问题
medium一般问题(默认)
high影响业务但可等待
urgent紧急业务完全中断

ApiKeyService

API Key 管理服务,用于创建和管理下游 API 访问密钥。API Key 可用于程序化访问 RustBill gRPC 接口,通过 ApiKeyAuthLayer 中间件认证,注入 DownstreamUser

RPC 列表

RPC描述权限
CreateApiKey创建新的 API Key(密钥仅返回一次)admin 或已授权 customer
ListApiKeys列出指定客户的 API Key 列表admin 或已授权 customer
RevokeApiKey吊销(禁用)API Keyadmin 或 API Key 所属客户

CreateApiKey

描述

创建新的 API Key。返回的 api_key 字段仅在创建时返回一次,之后无法再次获取,客户端应立即安全保存。

Request — CreateApiKeyRequest

字段类型编号必填描述
namestring1Key 名称(便于识别)
customer_idstring2关联客户 ID(admin 需指定,customer 用户自动设置)

Response — CreateApiKeyResponse

字段类型编号描述
api_keystring1完整 API Key(仅此一次返回,需立即保存)
key_prefixstring2Key 前缀(用于识别,如 "rk_live_a1b2c3"
idstring3API Key 记录 ID(UUID)

错误码

错误说明
InvalidArgument名称不能为空
NotFound指定客户不存在
PermissionDenied当前用户无权为此客户创建 Key(customer 需 can_create_api_keys=true

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"name":"生产环境 API Key","customer_id":"0192a123-..."}' \
  localhost:50051 rustbill.downstream.ApiKeyService/CreateApiKey
const resp = await api.createApiKey({
  name: "生产环境 API Key",
  customer_id: "0192a123-..."
});
// resp.api_key → "rk_live_a1b2c3d4e5f6..." (仅此一次)
// resp.key_prefix → "rk_live_a1b2c3"
// resp.id → "0192b456-..."
// 立即安全存储 api_key,之后无法再获取

ListApiKeys

描述

列出指定客户的所有 API Key(不包含完整密钥值,仅含前缀和元数据)。

Request — ListApiKeysRequest

字段类型编号必填描述
customer_idstring1客户 ID(UUID)

Response — ListApiKeysResponse

字段类型编号描述
keysrepeated ApiKeyInfo1API Key 信息列表

ApiKeyInfo

字段类型编号描述
idstring1Key 记录 ID(UUID)
key_prefixstring2Key 前缀(如 "rk_live_a1b2c3"
namestring3Key 名称
enabledbool4是否启用
created_atstring5创建时间(RFC3339)
last_used_atstring6最近使用时间(RFC3339)
expires_atstring7过期时间(RFC3339)

错误码

错误说明
PermissionDenied非管理员且无此客户的 API Key 管理权限

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"customer_id":"0192a123-..."}' \
  localhost:50051 rustbill.downstream.ApiKeyService/ListApiKeys
const resp = await api.listApiKeys({
  customer_id: "0192a123-..."
});
// resp.keys → [{ id, key_prefix, name, enabled, created_at, last_used_at, expires_at }]

RevokeApiKey

描述

吊销(禁用)指定的 API Key。吊销后该 Key 无法再用于认证。

Request — RevokeApiKeyRequest

字段类型编号必填描述
idstring1API Key 记录 ID(UUID)

Response — RevokeApiKeyResponse

无字段。

错误码

错误说明
NotFoundAPI Key 不存在
PermissionDenied非管理员且非该 Key 所属客户

示例

grpcurl -plaintext -H "authorization: Bearer <token>" \
  -d '{"id":"0192b456-...}' \
  localhost:50051 rustbill.downstream.ApiKeyService/RevokeApiKey
const resp = await api.revokeApiKey({
  id: "0192b456-..."
});

API Key 使用方式

API Key 在 HTTP 请求中作为 Bearer token 携带:

Authorization: Bearer rk_live_a1b2c3d4e5f6...

认证成功后,ApiKeyAuthLayer 中间件在 request extensions 中注入 DownstreamUser

DownstreamUser {
    api_key_id: Uuid,
    customer_id: Uuid,
    permissions: Vec<String>
}

服务端通过 require_downstream() 守卫访问下游用户信息,并根据 customer_id 自动限定数据范围。

权限对照

操作AdminCustomer(can_create_api_keys=true)Customer(can_create_api_keys=false)
CreateApiKey可创建任意客户仅创建自身客户禁止
ListApiKeys可查看任意客户仅查看自身客户禁止
RevokeApiKey可吊销任意 Key仅吊销自身 Key禁止

安全注意事项

  • api_key 完整值仅在 CreateApiKey 响应中返回一次,服务端不存储明文密钥(仅存哈希)
  • API Key 应存储在安全的环境中(如环境变量、密钥管理服务)
  • 建议定期轮换 API Key(创建新 Key + 吊销旧 Key)
  • 吊销操作将 enabled 设为 false,不物理删除记录