SHKeeper v2.5.32 Docker Compose 部署指南

SHKeeper v2.5.32 以 Docker Compose 部署 BTC regtest 收款链路的全程实录:九服务栈编排、初始化密码与钱包加密解锁、手动汇率、开票支付回调全流程闭环与六则排障实录。

SHKeeper加密货币支付系统自托管2026/10/24

SHKeeper v2.5.32 Docker Compose 部署指南

目标:在一台 Linux 服务器(本文以 Docker 容器化环境模拟真实服务器)上,用 Docker Compose 把 SHKeeper v2.5.32 核心与 BTC(regtest 模式)收款链路完整跑起来,并完成「创建发票 → 链上支付 → 确认 → 回调通知」的全流程实测验证。SHKeeper 的产品定位与选型背景见本专栏《SHKeeper v2.5.32 介绍与选型指南》,本文不重复。

一、环境信息

本次部署实测于 2026-10-24 完成,环境与软件清单如下:

项值说明
操作系统Linux(Docker 宿主机)内核 6.x,systemd
Docker 存储驱动overlayfs(io.containerd.snapshotter.v1)containerd 镜像存储
SHKeeper 核心vsyshost/shkeeper:2.5.32镜像 ID 8e4291d3fff4,约 1.9GB
BTC 全节点vsyshost/bitcoind:31.1镜像 ID 2266ffb1b21d,约 252MB
BTC 钱包适配器vsyshost/bitcoin-shkeeper:2.0.14镜像 ID cb68fbb7bb75,约 1.55GB
关系数据库mariadb:10.11官方 Chart 默认 10.9.3,本文以本地已有 10.11 替代(同为 10.x 稳定系,实测兼容)
消息队列redis:7-alpineCelery broker
监听端口8128(映射核心 5000)宿主侧访问入口
Compose 项目名shkeeper-v2.5.32-stack自定义网络 shkeeper-v2532-net(10.89.63.0/24)

⚠️ 说明:官方(GitHub 仓库 README)只提供 k3s + Helm Chart 一键部署路径,未发布官方 docker-compose.yml;但官方 helm-charts 仓库内含一个「渲染生成 docker-compose 配置」的 Chart 模板(docker-compose/shkeeper/templates/shkeeper.yaml,经 helm template 渲染后 docker compose up -d 启动,见其 README),说明官方认可 Compose 形态。本文采用「基于官方镜像与 Chart 中各组件环境变量契约、手工精简编排」的 Compose 栈:只启用 BTC(regtest),禁用其余全部币种,把最小可验证的收款链路压缩到 9 个服务。目标是先零成本跑通流程,再按需横向扩展其他币种。

二、部署架构与组件清单

SHKeeper 是「核心 + 每币种适配器 + 区块链节点」分层架构(详见《SHKeeper v2.5.32 介绍与选型指南》第三章)。只启用 BTC 时,一次部署包含 7 个常驻容器 + 2 个一次性初始化容器:

服务镜像角色说明
shkeepervsyshost/shkeeper:2.5.32核心(Web/API)Gunicorn 监听 5000,SQLite 存核心数据;管理台、发票、汇率、回调分发
bitcoin-shkeepervsyshost/bitcoin-shkeeper:2.0.14BTC 适配器(HTTP)Gunicorn 监听 6000;HD 地址派生、链上扫块、对核心提供统一接口
btc-tasksvsyshost/bitcoin-shkeeper:2.0.14BTC 适配器(Celery)异步任务:周期扫块、打款执行
bitcoindvsyshost/bitcoind:31.1BTC 全节点本文用 -regtest 模式,零同步成本
mariadbmariadb:10.11适配器业务库每个启用的币种一库,本文库名 bitcoin-shkeeper
redisredis:7-alpineCelery broker适配器侧任务队列
callback-receivervsyshost/shkeeper:2.5.32(复用)回调接收器(实测用)Python 标准库 HTTP 服务监听 9000,验证回调并返回 202
db-createmariadb:10.11一次性建库 bitcoin-shkeeper
adapter-initvsyshost/bitcoin-shkeeper:2.0.14一次性python manage.py init_db,对适配器库执行建表与 Alembic 迁移

组件间连接关系(这是理解部署故障的关键):

  1. 核心 → 适配器:BTC_API_SERVER_HOST=bitcoin-shkeeper、BTC_SERVER_PORT=6000,Basic 认证 BTC_USERNAME/BTC_PASSWORD;
  2. 适配器 → 核心:SHKEEPER_HOST=shkeeper:5000,携带 SHKEEPER_BACKEND_KEY(两侧必须一致);
  3. 适配器/任务 → 节点:FULLNODE_URL=http://<rpc用户>:<rpc密码>@bitcoind:18443;
  4. bitcoind → 核心:-walletnotify 脚本在交易入块时回调核心 /api/v1/walletnotify/BTC/<txid>,同样要求 Backend Key 一致。

三、前置准备

3.1 镜像获取

从 Docker Hub 拉取官方镜像(若网络环境受限,可先在能联网的机器 docker pull + docker save,再到目标机 docker load,本文实测即采用归档导入方式):

docker load -i vsyshost_shkeeper_2.5.32.tar
docker load -i vsyshost_bitcoind_31.1.tar
docker load -i vsyshost_bitcoin-shkeeper_2.0.14.tar
docker pull mariadb:10.11
docker pull redis:7-alpine

加载后核对:

docker images --format '{{.Repository}}:{{.Tag}} {{.ID}}' | grep -E 'vsyshost|mariadb|redis'

实测回显(节选):

vsyshost/shkeeper:2.5.32 8e4291d3fff4
vsyshost/bitcoind:31.1 2266ffb1b21d
vsyshost/bitcoin-shkeeper:2.0.14 cb68fbb7bb75

⚠️ 排障实录 ①:本次实验宿主机的 Docker daemon 代理配置失效(docker pull 报 proxyconnect tcp: dial tcp ...:7897: i/o timeout),且 docker load 后出现过「docker images 能列出镜像、按名称 docker inspect 却报 no such object」的镜像索引异常。解决办法是用镜像 ID 重新打一次 tag 即可恢复名称索引:docker tag 2266ffb1b21d vsyshost/bitcoind:31.1。若你也遇到镜像列表与 inspect 结果不一致,先按 ID docker tag 一遍。

3.2 目录结构

选定部署根目录(本文为 /opt/shkeeper-stack),布局如下:

/opt/shkeeper-stack/
├── docker-compose.yml
└── data/
    ├── mariadb/        # MariaDB 数据
    ├── bitcoind/       # 节点数据(regtest 链)
    ├── bitcoin/        # 适配器与节点共享的 BTC 数据目录
    └── shkeeper/       # 核心实例目录(SQLite、会话)

四、docker-compose.yml 全量文件

以下为本篇实测所用完整文件,可直接复制使用(只启用 BTC,其余币种一律 disabled):

name: shkeeper-v2.5.32-stack

networks:
  shkeeper-net:
    name: shkeeper-v2532-net
    driver: bridge
    ipam:
      config:
        - subnet: 10.89.63.0/24

services:
  mariadb:
    image: mariadb:10.11
    container_name: shkeeper-2.5.32-mariadb
    hostname: mariadb
    restart: unless-stopped
    environment:
      MARIADB_ROOT_PASSWORD: shkeeper
    volumes:
      - ./data/mariadb:/var/lib/mysql
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      interval: 10s
      timeout: 5s
      retries: 10
    networks: [shkeeper-net]

  bitcoind:
    image: vsyshost/bitcoind:31.1
    container_name: shkeeper-2.5.32-bitcoind
    hostname: bitcoind
    restart: unless-stopped
    command:
      - /shkeeper/bitcoind
      - -rpcport=18443
      - -rpcpassword=shkeeper
      - -rpcuser=shkeeper
      - -rpcbind=0.0.0.0
      - -rpcallowip=0.0.0.0/0
      - -txindex=1
      - -regtest
      - -fallbackfee=0.0002
    volumes:
      - ./data/bitcoind:/root/.bitcoin
    networks: [shkeeper-net]

  db-create:
    image: mariadb:10.11
    container_name: shkeeper-2.5.32-db-create
    restart: "no"
    depends_on:
      mariadb:
        condition: service_healthy
    entrypoint:
      - sh
      - -c
      - 'mariadb -h mariadb -u root -pshkeeper -e "CREATE DATABASE IF NOT EXISTS \`bitcoin-shkeeper\`" && echo DB-CREATE-OK'
    networks: [shkeeper-net]

  adapter-init:
    image: vsyshost/bitcoin-shkeeper:2.0.14
    container_name: shkeeper-2.5.32-adapter-init
    restart: "no"
    depends_on:
      db-create:
        condition: service_completed_successfully
    command: ["python", "manage.py", "init_db"]
    environment:
      WALLET: BTC
      BTC_NETWORK: regtest
      BTC_USERNAME: shkeeper
      BTC_PASSWORD: shkeeper
      SHKEEPER_BACKEND_KEY: q1w2e3r4
      FULLNODE_URL: http://shkeeper:shkeeper@bitcoind:18443
      REDIS_HOST: redis
    networks: [shkeeper-net]

  redis:
    image: redis:7-alpine
    container_name: shkeeper-2.5.32-redis
    restart: unless-stopped
    networks: [shkeeper-net]

  bitcoin-shkeeper:
    image: vsyshost/bitcoin-shkeeper:2.0.14
    container_name: shkeeper-2.5.32-btc-adapter
    hostname: bitcoin-shkeeper
    restart: unless-stopped
    depends_on:
      adapter-init:
        condition: service_completed_successfully
      redis:
        condition: service_started
      bitcoind:
        condition: service_started
    command:
      - gunicorn
      - --access-logfile=-
      - --workers=1
      - --threads=16
      - --timeout=600
      - --bind=0.0.0.0:6000
      - run:server
    environment:
      WALLET: BTC
      BTC_NETWORK: regtest
      BTC_USERNAME: shkeeper
      BTC_PASSWORD: shkeeper
      SHKEEPER_BACKEND_KEY: q1w2e3r4
      SHKEEPER_HOST: shkeeper:5000
      FULLNODE_URL: http://shkeeper:shkeeper@bitcoind:18443
      REDIS_HOST: redis
    volumes:
      - ./data/bitcoin:/root/.bitcoin
    networks: [shkeeper-net]

  btc-tasks:
    image: vsyshost/bitcoin-shkeeper:2.0.14
    container_name: shkeeper-2.5.32-btc-tasks
    restart: unless-stopped
    depends_on:
      adapter-init:
        condition: service_completed_successfully
      redis:
        condition: service_started
      bitcoind:
        condition: service_started
    command: ["celery", "-A", "celery_worker.celery", "worker", "--loglevel=info", "-B"]
    environment:
      WALLET: BTC
      BTC_NETWORK: regtest
      BTC_USERNAME: shkeeper
      BTC_PASSWORD: shkeeper
      SHKEEPER_BACKEND_KEY: q1w2e3r4
      SHKEEPER_HOST: shkeeper:5000
      FULLNODE_URL: http://shkeeper:shkeeper@bitcoind:18443
      REDIS_HOST: redis
      C_FORCE_ROOT: "1"
    volumes:
      - ./data/bitcoin:/root/.bitcoin
    networks: [shkeeper-net]

  shkeeper:
    image: vsyshost/shkeeper:2.5.32
    container_name: shkeeper-2.5.32-web
    hostname: shkeeper
    restart: unless-stopped
    depends_on:
      - bitcoin-shkeeper
      - btc-tasks
    ports:
      - "8128:5000"
    environment:
      BTC_WALLET: enabled
      LTC_WALLET: disabled
      DOGE_WALLET: disabled
      FIRO_WALLET: disabled
      FIRO_SPARK_WALLET: disabled
      TRX_WALLET: disabled
      USDT_WALLET: disabled
      USDC_WALLET: disabled
      ETH_WALLET: disabled
      ETH_USDT_WALLET: disabled
      ETH_USDC_WALLET: disabled
      ETH_PYUSD_WALLET: disabled
      ETH_DAI_WALLET: disabled
      MONERO_WALLET: disabled
      BNB_WALLET: disabled
      BNB_USDT_WALLET: disabled
      BNB_USDC_WALLET: disabled
      XRP_WALLET: disabled
      MATIC_WALLET: disabled
      POLYGON_USDT_WALLET: disabled
      POLYGON_USDC_WALLET: disabled
      AVAX_WALLET: disabled
      AVALANCHE_USDT_WALLET: disabled
      AVALANCHE_USDC_WALLET: disabled
      SOL_WALLET: disabled
      SOLANA_USDT_WALLET: disabled
      SOLANA_USDC_WALLET: disabled
      ARBETH_WALLET: disabled
      OPETH_WALLET: disabled
      TON_WALLET: disabled
      BITCOINLIGHTNING_WALLET: disabled
      BTC_USERNAME: shkeeper
      BTC_PASSWORD: shkeeper
      BTC_API_SERVER_HOST: bitcoin-shkeeper
      BTC_SERVER_PORT: "6000"
      SHKEEPER_BTC_BACKEND_KEY: q1w2e3r4
      EXTRA_CURRENCIES: "CNY"
      METRICS_USERNAME: admin
      METRICS_PASSWORD: q1w2e3r4
    volumes:
      - ./data/shkeeper:/shkeeper.io/instance
    networks: [shkeeper-net]

  callback-receiver:
    image: vsyshost/shkeeper:2.5.32
    container_name: shkeeper-2.5.32-callback
    restart: unless-stopped
    entrypoint:
      - python
      - -c
      - |
        import json
        from http.server import BaseHTTPRequestHandler, HTTPServer
        class H(BaseHTTPRequestHandler):
            def do_POST(self):
                body = self.rfile.read(int(self.headers.get("Content-Length", 0)))
                print("=== CALLBACK ===", flush=True)
                print("sig:", self.headers.get("X-Shkeeper-Signature"), flush=True)
                print("ts :", self.headers.get("X-Shkeeper-Timestamp"), flush=True)
                print(json.dumps(json.loads(body), indent=2, ensure_ascii=False), flush=True)
                self.send_response(202)
                self.end_headers()
                self.wfile.write(b"accepted")
            def do_GET(self):
                self.send_response(200)
                self.end_headers()
                self.wfile.write(b"callback receiver alive")
        HTTPServer(("0.0.0.0", 9000), H).serve_forever()
    networks: [shkeeper-net]

要点解读:

  1. 所有 <币种>_WALLET 开关:核心启动时按 enabled/disabled 决定初始化哪些币种的钱包配置;只启用 BTC_WALLET: enabled 时,/api/v1/crypto 只会返回 BTC。
  2. Backend Key 的两侧一致性:核心侧变量名是 SHKEEPER_BTC_BACKEND_KEY,适配器侧是 SHKEEPER_BACKEND_KEY,两侧值必须相同(本文用 q1w2e3r4)。它用于 bitcoind→适配器→核心的 walletnotify 与 payoutnotify 回调鉴权,值不一致时回调会 403。
  3. db-create 的库名必须加反引号:适配器约定库名为 <小写币种>-shkeeper(如 bitcoin-shkeeper),含连字符的库名在 SQL 中必须用反引号包裹。
  4. 回调接收器仅为验证回调内容而设;生产环境把 callback_url 换成你的业务系统即可。

五、启动与状态验证

cd /opt/shkeeper-stack
docker compose up -d

实测回显(节选):

 Container shkeeper-2.5.32-mariadb Healthy
 Container shkeeper-2.5.32-db-create Starting
 Container shkeeper-2.5.32-db-create Started
 Container shkeeper-2.5.32-adapter-init Starting
 Container shkeeper-2.5.32-adapter-init Exited
 Container shkeeper-2.5.32-btc-adapter Starting
 Container shkeeper-2.5.32-btc-tasks Starting
 Container shkeeper-2.5.32-web Starting

⚠️ 排障实录 ②:db-create 首次执行失败,日志为 ERROR 1064 (42000) ... syntax to use near '-shkeeper'——库名 bitcoin-shkeeper 的连字符被 SQL 解析为减号。把建库语句改为 CREATE DATABASE IF NOT EXISTS `bitcoin-shkeeper` (反引号包裹)后重新 docker compose up -d 即可。修改 compose 内 command/entrypoint 后建议先 docker compose down 再 up -d,否则已存在的旧配置容器不会被重建。

adapter-init 一次性容器负责适配器库的建表与迁移,成功日志:

INFO  [alembic.runtime.migration] Running upgrade  -> 8921e5b04057, increase raw column size to mediumblob
Creating tables...
Tables created.
Alembic migrations applied.

检查常驻容器状态:

docker ps --filter 'name=shkeeper-2.5.32' --format '{{.Names}}\t{{.Status}}'

实测回显:

shkeeper-2.5.32-mariadb  Up 23 seconds (healthy)
shkeeper-2.5.32-bitcoind Up 24 seconds
shkeeper-2.5.32-redis    Up 24 seconds
shkeeper-2.5.32-callback Up 24 seconds
shkeeper-2.5.32-btc-adapter Up 11 seconds
shkeeper-2.5.32-btc-tasks   Up 11 seconds
shkeeper-2.5.32-web      Up 10 seconds

探测核心服务(宿主机访问 8128 端口):

curl -s http://127.0.0.1:8128/api/v1/crypto

实测回显:

{"crypto":["BTC"],"crypto_list":[{"display_name":"Bitcoin","name":"BTC"}],"status":"success"}

返回 "crypto":["BTC"] 即代表核心、适配器、节点三层已连通(该接口无需认证,币种列表来自核心对各适配器可用性的实时聚合)。

六、管理台初始化(密码 + 钱包加密解锁)

SHKeeper v2.5.32 首次启动时管理员无密码,必须先完成两件事:设置管理员密码、钱包加密解锁(两段式)。以下全部用 curl 实测(浏览器操作路径相同)。

6.1 设置管理员密码

curl -s -c cookies.txt -o /dev/null -w 'GET /login -> %{http_code} -> %{redirect_url}\n' http://127.0.0.1:8128/login

实测回显:

GET /login -> 302 -> http://127.0.0.1:8128/set-password

未设密码时访问登录页会强制跳到 /set-password。提交新密码(示例用 admin / q1w2e3r4,生产请用强密码):

curl -s -b cookies.txt -c cookies.txt -o /dev/null -w 'POST /set-password -> %{http_code} -> %{redirect_url}\n' \
  -d 'pw1=q1w2e3r4&pw2=q1w2e3r4' http://127.0.0.1:8128/set-password
curl -s -b cookies.txt -c cookies.txt -o /dev/null -w 'POST /login -> %{http_code} -> %{redirect_url}\n' \
  -d 'name=admin&password=q1w2e3r4' http://127.0.0.1:8128/login

实测回显:

POST /set-password -> 302 -> http://127.0.0.1:8128/login
POST /login -> 302 -> http://127.0.0.1:8128/unlock

登录成功后强制进入 /unlock(钱包加密解锁页),这是 v2.5.x 的安全设计:适配器数据库里的 HD 私钥字段是加密存储的,必须先解锁。

6.2 钱包加密解锁(两段式,容易踩坑)

⚠️ 排障实录 ③:/unlock 是两段式表单。第一段提交(带 encryption=yes + 两次密码 + 确认勾选)只是「设定加密密码」;提交后页面回到 /unlock 再渲染一个「输入加密密码」的表单,必须再提交一次只带 key 字段的请求才算解锁。只做第一段时,适配器会持续循环告警(实测日志):

WARNING: Get password from shkeeper
WARNING: Encryption enabled, but password is not entered yet
WARNING: Encryption password not available yet, waiting 20 seconds...

第一段(设定加密密码):

curl -s -b cookies.txt -o /dev/null -w 'POST /unlock #1 -> %{http_code} -> %{redirect_url}\n' \
  --data-urlencode 'encryption=yes' \
  --data-urlencode 'key=q1w2e3r4' \
  --data-urlencode 'key2=q1w2e3r4' \
  --data-urlencode 'confirmation=yes' \
  http://127.0.0.1:8128/unlock

第二段(输入密码解锁):

curl -s -b cookies.txt -o /dev/null -w 'POST /unlock #2 -> %{http_code} -> %{redirect_url}\n' \
  --data-urlencode 'key=q1w2e3r4' \
  http://127.0.0.1:8128/unlock

解锁成功后,核心把密码推给适配器,适配器立即连上节点并开始工作(适配器实测日志):

INFO: Retrieved blockchain info: {'chain': 'regtest', 'blocks': 0, 'headers': 0, ...}
INFO: Node now_ts 1792830752.9228573
WARNING: Node not synced yet (time_last_block=2011-02-02 23:16:42, delta=496142151s), waiting 120 seconds...

regtest 刚起链时区块数为 0、创世块时间戳是 2011 年,适配器会按「最新块时间过旧」误判未同步并等待 120 秒——挖出第一批块后该告警自动消失,属 regtest 特有现象。

6.3 获取 API Key

登录态下调用管理接口拿到 BTC 钱包的 API Key(后续业务接口全部用它认证):

curl -s -b cookies.txt http://127.0.0.1:8128/api/v1/BTC/payment-gateway

实测回显:

{"enabled":true,"status":"success","token":"NjOwsKx8v5Kctk9aY0O20w"}

顺手确认节点状态:

curl -s -b cookies.txt http://127.0.0.1:8128/api/v1/BTC/status

实测回显:

{"amount":0,"name":"BTC","server":"Synced"}

七、BTC regtest 准备:挖币与手动汇率

7.1 用 regtest 挖出可支付余额

进入 bitcoind 容器操作(该镜像的可执行文件在 /shkeeper/ 下,不在 PATH):

docker exec shkeeper-2.5.32-bitcoind /shkeeper/bitcoin-cli -regtest \
  -rpcuser=shkeeper -rpcpassword=shkeeper createwallet shkeeper
docker exec shkeeper-2.5.32-bitcoind /shkeeper/bitcoin-cli -regtest \
  -rpcuser=shkeeper -rpcpassword=shkeeper -generate 101
docker exec shkeeper-2.5.32-bitcoind /shkeeper/bitcoin-cli -regtest \
  -rpcuser=shkeeper -rpcpassword=shkeeper -rpcwallet=shkeeper getbalance

实测回显(最后一命令):

50.00000000

regtest 挖出 101 块后 coinbase 成熟,获得 50 BTC 测试余额,可作为「买家钱包」使用。

💡 注意:createwallet shkeeper 创建的钱包与 compose 中 -wallet=shkeeper 参数对应,数据落在 ./data/bitcoind,重启容器会自动加载,无需重复创建。

7.2 配置手动汇率(离线环境必需)

⚠️ 排障实录 ④:创建发票时若核心无法访问外网汇率源,会直接失败(实测报错原文):

{"message":"HTTPSConnectionPool(host='api.binance.com', port=443): Max retries exceeded with
url: /api/v3/ticker/price?symbol=BTCUSDT ...","status":"error"}

SHKeeper 默认汇率源是 Binance(另有 Coinbase/Kraken/KuCoin 自动源与手动源)。内网/离线部署的解法是把 BTC 对 USD 的汇率源切换为 manual 并手工设定汇率,此后创建发票不再发起外网请求。

登录态下提交汇率表单(与网页 /rates/USD 表单等价):

curl -s -b cookies.txt -o /dev/null -w 'POST /rates/USD -> %{http_code}\n' \
  --data-urlencode 'rates__BTC__source=manual' \
  --data-urlencode 'rates__BTC__rate=100000' \
  --data-urlencode 'rates__BTC__fee_policy=PERCENT_FEE' \
  --data-urlencode 'rates__BTC__fee=2' \
  --data-urlencode 'rates__BTC__fixed_fee=0' \
  http://127.0.0.1:8128/rates/USD

实测回显:

POST /rates/USD -> 302

以上设置含义:1 BTC = 100000 USD,商户加价 2%(发票金额会按 fiat × (1+2%) ÷ 汇率 折算为加密币数量)。

八、全流程实测:开票 → 支付 → 确认 → 回调

8.1 创建发票

以商户身份创建一笔 18.25 USD 的订单发票(external_id 是你业务系统里的订单号):

curl -s -X POST http://127.0.0.1:8128/api/v1/BTC/payment_request \
  -H 'X-Shkeeper-API-Key: NjOwsKx8v5Kctk9aY0O20w' \
  -H 'Content-Type: application/json' \
  -d '{"external_id":"1001","fiat":"USD","amount":"18.25","callback_url":"http://callback-receiver:9000/hook"}'

实测回显:

{
    "amount": "0.00018615",
    "display_name": "Bitcoin",
    "exchange_rate": "100000",
    "id": 1,
    "recalculate_after": 0,
    "status": "success",
    "wallet": "bcrt1qtsag9mtsgdr5etcxmswank0jmvddmzyyedzj8r"
}

金额校验:18.25 USD × 1.02(费率)÷ 100000 = 0.00018615 BTC,与 amount 一致;wallet 即为本次发票的专属收款地址(bcrt1q... 前缀确认 regtest 链)。

8.2 模拟买家支付并确认

向发票地址支付 0.00018615 BTC,然后出块确认:

docker exec shkeeper-2.5.32-bitcoind /shkeeper/bitcoin-cli -regtest \
  -rpcuser=shkeeper -rpcpassword=shkeeper -rpcwallet=shkeeper \
  sendtoaddress bcrt1qtsag9mtsgdr5etcxmswank0jmvddmzyyedzj8r 0.00018615
docker exec shkeeper-2.5.32-bitcoind /shkeeper/bitcoin-cli -regtest \
  -rpcuser=shkeeper -rpcpassword=shkeeper -generate 2

第一条命令实测回显(交易 ID):

b8d82118fd37dca7b3c5db08e7edda78ec6ad207f7166e52bf7bd99cab9522bc

⚠️ 排障实录 ⑤(重要):适配器的扫块游标在首次启动时以「当时的链高」初始化(适配器实测日志 INFO: No last_scanned_block found, initializing with 103)。上例中支付交易入块 102,早于游标初始化,适配器永远扫不到这笔交易,发票不会转 PAID。结论:必须先把整条栈启动、适配器完成初始化之后,再发起支付。若历史交易必须补录,可对适配器做重扫或直接复用发票地址重新支付一笔新的交易(本文实测即向同一发票地址二次支付同金额,新交易入块 104,在扫块游标之内,被正常捕获)。

第二笔支付与确认:

docker exec shkeeper-2.5.32-bitcoind /shkeeper/bitcoin-cli -regtest \
  -rpcuser=shkeeper -rpcpassword=shkeeper -rpcwallet=shkeeper \
  sendtoaddress bcrt1qtsag9mtsgdr5etcxmswank0jmvddmzyyedzj8r 0.00018615
docker exec shkeeper-2.5.32-bitcoind /shkeeper/bitcoin-cli -regtest \
  -rpcuser=shkeeper -rpcpassword=shkeeper -generate 2

实测回显(交易 ID):

42e4f277fe55b1d3a479ff1d15dca3e28faa7e524489d917e8246448072f4418

适配器按 Celery 周期(默认每分钟)扫块,捕获该交易后,发票状态推进为 PAID(适配器实测日志):

✅ SCAN COMPLETED: 0.0s, block 19f0ed28f22d6ca7c8e365b7c0dd810ce8cd2443f11b27a0d6b1302d26d6eabb, total_txs=1, related_txs=0, related_utxo=0, correlation=0.00%

8.3 验证回调通知

核心向 callback_url 发出 HTTP POST 回调,回调接收器完整收到(实测日志,关键行):

=== CALLBACK ===
sig: <HMAC-SHA256 签名>
ts : <时间戳>
{
  "balance_crypto": "0.00018615",
  "balance_fiat": "18.615",
  "crypto": "BTC",
  "external_id": "1001",
  "fee_fixed": "0",
  "fee_percent": "2",
  "fee_policy": "PERCENT_FEE",
  "fiat": "USD",
  "overpaid_fiat": "0.00",
  "paid": true,
  "status": "PAID",
  "transactions": [
    {
      "amount_crypto": "0.00018615",
      "amount_fiat": "18.615",
      "amount_fiat_without_fee": "18.25",
      "crypto": "BTC",
      "date": "2026-10-24 08:40:32",
      "fee_fiat": "0.365",
      "trigger": true,
      "txid": "42e4f277fe55b1d3a479ff1d15dca3e28faa7e524489d917e8246448072f4418"
    }
  ]
}
10.89.63.8 - - [24/Oct/2026 08:40:33] "POST /hook HTTP/1.1" 202 -

回调要点(对接业务系统的验收标准):

  1. 状态机:status: PAID、paid: true;部分支付为 PARTIAL、超付为 OVERPAID;
  2. 新版回调带 X-Shkeeper-Signature(HMAC-SHA256,密钥为钱包 API Key)与 X-Shkeeper-Timestamp 头,务必验签后再信任;旧版 X-Shkeeper-Api-Key 头仅为兼容保留;
  3. 接收方必须返回 202 Accepted,否则核心每 60 秒重发一次。

8.4 查询发票终态

curl -s -H 'X-Shkeeper-API-Key: NjOwsKx8v5Kctk9aY0O20w' http://127.0.0.1:8128/api/v1/invoices/1001

实测回显:

{"invoices":[{"amount_fiat":"18.25","balance_fiat":"18.615","external_id":"1001","fiat":"USD","status":"PAID","txs":[{"addr":"bcrt1qtsag9mtsgdr5etcxmswank0jmvddmzyyedzj8r","amount":"0.00018615","crypto":"BTC","status":"CONFIRMED","txid":"42e4f277fe55b1d3a479ff1d15dca3e28faa7e524489d917e8246448072f4418"}]}],"status":"success"}

至此「创建发票 → 链上支付 → 确认 → 回调 → 终态查询」全流程闭环,接口细节的完整清单见《SHKeeper v2.5.32 API 使用详解》。

九、打款(Payout)在 regtest 下的实测边界

打款接口的 API 语义可以完整实测,但链上出账闭环在 regtest 下受适配器限制无法完成(如实记录):

curl -s -b cookies.txt -X POST -H 'Content-Type: application/json' \
  -d '{"destination":"bcrt1q9x3uaaua27cqsa04tad8ntntakc7hwk8nch70s","amount":"0.0001","fee":"0.00001","external_id":"PO-1003","callback_url":"http://callback-receiver:9000/hook"}' \
  http://127.0.0.1:8128/api/v1/BTC/payout

实测回显:

{"external_id":"PO-1003","task_id":"7309c1f1-8003-420b-9879-d09e922f6fdf"}
curl -s -H 'X-Shkeeper-API-Key: NjOwsKx8v5Kctk9aY0O20w' \
  'http://127.0.0.1:8128/api/v1/BTC/payout/status?external_id=PO-1003'

实测回显(先受理):

{"amount":"0.0001000000","crypto":"BTC","destination":"bcrt1q9x3uaaua27cqsa04tad8ntntakc7hwk8nch70s","external_id":"PO-1003","id":4,"status":"IN_PROGRESS","txid":null}

Celery 任务执行后终态转为失败,任务查询接口可看到原因:

curl -s -b cookies.txt http://127.0.0.1:8128/api/v1/BTC/task/7309c1f1-8003-420b-9879-d09e922f6fdf

实测回显:

{"result":"Address bcrt1q9x3uaaua27cqsa04tad8ntntakc7hwk8nch70s is not valid address","status":"FAILURE"}

⚠️ 排障实录 ⑥:bitcoin-shkeeper 2.0.14 适配器内部使用 bitcoinlib 做打款前地址校验,该库不识别 regtest 的地址前缀(bech32 的 bcrt1 与 legacy 前缀均被拒),因此 regtest 模式只适合验证「收款」链路,打款链路必须在 testnet 或主网验证。这与《SHKeeper v2.5.32 介绍与选型指南》中「打款能力依赖币种适配成熟度」的结论互相印证。

同场加映的两个参数坑(均实测):payout 缺 fee 字段报 {"message":"'fee'","status":"error"};fee 传 "normal" 这类字符串报 [<class 'decimal.ConversionSyntax'>]——BTC 族打款 fee 必须是数值字符串(BTC 计价手续费,如 0.00001)。重复 external_id 会报 {"message":"Payout with this external_id already exists: PO-1001","status":"error"},幂等保护符合预期。

十、日常运维

10.1 启停与日志

docker compose stop      # 停止(保留容器与数据)
docker compose start     # 启动
docker compose down      # 删除容器与网络(数据卷仍在 ./data)
docker compose up -d     # 再次拉起
docker compose logs -f shkeeper      # 跟核心日志
docker compose logs -f bitcoin-shkeeper   # 跟适配器日志
docker compose logs -f btc-tasks     # 跟 Celery 扫块/打款日志

重启后注意:核心的管理台会话全部失效(SECRET_KEY 未固定时随机生成);适配器保存的加密密码随数据保留,无需重新 /unlock(除非重建 data/shkeeper 或适配器库)。

10.2 升级

  1. docker compose down;
  2. 替换镜像 tag(如 vsyshost/shkeeper:2.5.33),docker compose up -d;
  3. 核心 SQLite 与适配器 MariaDB 的 schema 迁移:核心由 Gunicorn 启动前的迁移逻辑处理,适配器侧由 adapter-init 的 init_db 处理——升级时保留 adapter-init 服务并让它先跑完;
  4. 小版本(v2.5.x)兼容性风险低,跨大版本前先读官方 Release Notes 并全量备份。

10.3 备份

最小备份集 = 三份数据目录:data/shkeeper/(核心 SQLite + 会话)、data/mariadb/(适配器地址与交易)、data/bitcoind/(可选,节点数据可再生但重新同步耗时;regtest 下很小)。适配器库中存放加密后的 HD 私钥,钱包加密密码必须另行离线保管——丢失密码且库损坏时地址私钥不可恢复。

十一、部署验收清单

  • docker compose ps 全部常驻容器 Up,db-create/adapter-init 两个 init 容器 Exited (0);
  • /api/v1/crypto 返回 {"crypto":["BTC"],...};
  • 管理台 /set-password → /login → /unlock 两段式完成,/wallets 页 BTC 显示 Synced;
  • POST /api/v1/BTC/payment_request 返回 status:success 与 bcrt1 地址;
  • 支付并出块后,发票 status:PAID、回调返回体 paid:true 且接收方回 202;
  • /metrics(Basic 认证)输出 bitcoin_fullnode_status 1.0。

生产化替换建议:regtest 改为主网/testnet 节点参数;MARIADB_ROOT_PASSWORD、SHKEEPER_BACKEND_KEY、管理员密码全部换强随机值;固定 SECRET_KEY 环境变量避免重启后全站会话失效;为 8128 套 Nginx/TLS 并开启管理台内置 TOTP 双因素。

十二、参考与关联