区块浏览器后端:区块/交易/地址/合约查询、链数据统计.
2026/5/15 12:17:12 网站建设 项目流程

我直接用大白话给你讲清楚一个区块浏览器后端从 0 到开源完整维护的全流程,含可运行代码。


一、先回答:做这个有什么用?

  1. 区块浏览器是什么

你用过 Etherscan、BscScan、Solscan 吧?输入一个交易哈希,能看到这笔交易转了多少钱、给谁、什么时候、Gas
多少;输入一个地址,能看到余额、所有历史交易;输入一个合约,能看到代码、调用次数。这就是区块浏览器。

区块链节点本身(比如 Geth)只存"原始数据",查询效率极低:你想查"这个地址近 30
天所有交易",节点得把全网数据扫一遍,慢到无法用。区块浏览器后端的作用就是:把链上数据"整理 + 索引 +
缓存"到普通数据库里,让查询从"几分钟"变成"几毫秒"。

  1. 做这个项目的 5 大作用(真实价值)

┌────────────┬─────────────────────────────────────────────────────────────────────────────┐
│ 作用 │ 大白话解释 │
├────────────┼─────────────────────────────────────────────────────────────────────────────┤
│ 数据可读化 │ 链上数据是十六进制乱码,普通人看不懂,你把它翻译成"Alice 转 1.5 ETH 给 Bob" │
├────────────┼─────────────────────────────────────────────────────────────────────────────┤
│ 查询加速 │ 节点查"某地址历史交易"要扫全链,你建好索引后 5ms 返回 │
├────────────┼─────────────────────────────────────────────────────────────────────────────┤
│ 业务支撑 │ 钱包 App、交易所、DeFi 项目都需要这种 API(自己跑节点太贵) │
├────────────┼─────────────────────────────────────────────────────────────────────────────┤
│ 数据分析 │ 链上活跃度、Gas 趋势、合约调用排行——这些都靠你聚合 │
├────────────┼─────────────────────────────────────────────────────────────────────────────┤
│ 简历加分 │ 这是 Web3 后端工程师的"标准项目",写完简历能直接进交易所/钱包公司 │
└────────────┴─────────────────────────────────────────────────────────────────────────────┘

  1. 谁在用类似系统
  • Etherscan(年收入估算 5000 万美金,靠 API 订阅)
  • 币安/OKX 内部钱包(都有自研浏览器后端)
  • The Graph 协议(去中心化版本的索引服务)

二、技术选型:为什么选这些库(最佳组合)

▎ 行业标准(Etherscan、Blockscout、Ankr 都这样选)

┌────────────┬─────────────────────────┬──────────────────────────────────────────────────────────┐
│ 组件 │ 选型 │ 大白话理由 │
├────────────┼─────────────────────────┼──────────────────────────────────────────────────────────┤
│ 语言 │ Go │ 以太坊节点 Geth 本身就是 Go 写的,库最全;并发处理区块快 │
├────────────┼─────────────────────────┼──────────────────────────────────────────────────────────┤
│ Web 框架 │ Gin │ Go 里最快、最稳,star 80k+ │
├────────────┼─────────────────────────┼──────────────────────────────────────────────────────────┤
│ 区块链交互 │ go-ethereum (geth lib) │ 官方库,兼容所有 EVM 链(ETH/BSC/Polygon/Arbitrum) │
├────────────┼─────────────────────────┼──────────────────────────────────────────────────────────┤
│ 数据库 │ PostgreSQL │ 支持 JSONB、分区表,存几亿条交易没问题 │
├────────────┼─────────────────────────┼──────────────────────────────────────────────────────────┤
│ ORM │ GORM │ Go 里最流行,写起来像 ActiveRecord │
├────────────┼─────────────────────────┼──────────────────────────────────────────────────────────┤
│ 缓存 │ Redis │ 热门地址/交易缓存,扛住高并发 │
├────────────┼─────────────────────────┼──────────────────────────────────────────────────────────┤
│ 消息队列 │ NATS 或 Kafka │ 区块事件分发给多个消费者 │
├────────────┼─────────────────────────┼──────────────────────────────────────────────────────────┤
│ API 文档 │ swag(生成 Swagger) │ 注释即文档 │
├────────────┼─────────────────────────┼──────────────────────────────────────────────────────────┤
│ 配置 │ viper │ YAML/ENV/命令行都能读 │
├────────────┼─────────────────────────┼──────────────────────────────────────────────────────────┤
│ 日志 │ zap │ Uber 出品,最快的结构化日志 │
├────────────┼─────────────────────────┼──────────────────────────────────────────────────────────┤
│ 测试 │ testify + dockertest │ 单元测试 + 真实 Postgres 容器测试 │
├────────────┼─────────────────────────┼──────────────────────────────────────────────────────────┤
│ CI/CD │ GitHub Actions │ 免费、和 GitHub 原生集成 │
├────────────┼─────────────────────────┼──────────────────────────────────────────────────────────┤
│ 容器 │ Docker + docker-compose │ 一键启动整个环境 │
├────────────┼─────────────────────────┼──────────────────────────────────────────────────────────┤
│ 监控 │ Prometheus + Grafana │ 行业事实标准 │
└────────────┴─────────────────────────┴──────────────────────────────────────────────────────────┘


三、从 0 到 1 的完整代码

项目名我们叫 chainscan。

步骤 1:项目初始化与目录结构

mkdir chainscan && cd chainscan
go mod init github.com/yourname/chainscan
git init

目录结构(这是 Go 项目的"事实标准",照抄 golang-standards/project-layout
(https://github.com/golang-standards/project-layout)):

chainscan/
├── cmd/ # 程序入口
│ ├── api/main.go # API 服务
│ └── indexer/main.go # 索引器服务
├── internal/ # 私有代码(不对外)
│ ├── config/ # 配置加载
│ ├── domain/ # 领域模型 (Block/Tx/Address)
│ ├── repository/ # 数据库访问层
│ ├── service/ # 业务逻辑
│ ├── handler/ # HTTP 处理器
│ ├── indexer/ # 链上数据抓取
│ └── cache/ # Redis 封装
├── pkg/ # 可对外复用的库
│ └── ethclient/ # 节点客户端封装
├── migrations/ # 数据库迁移 SQL
├── docs/ # Swagger 文档
├── deploy/ # Dockerfile/k8s
├── .github/ # CI/PR 模板
├── configs/ # 配置文件
├── Makefile
├── docker-compose.yml
├── README.md
├── LICENSE
└── go.mod

大白话:cmd 放 main 函数;internal 放别人不能 import 的内部代码;pkg 放可以给别人复用的;migrations 放建表 SQL。

步骤 2:配置文件

configs/config.yaml:
server:
port: 8080
mode: debug

postgres:
dsn: “host=localhost user=chainscan password=secret dbname=chainscan port=5432 sslmode=disable”

redis:
addr: “localhost:6379”

ethereum:
rpc_url: “https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY”
ws_url: “wss://eth-mainnet.g.alchemy.com/v2/YOUR_KEY”
start_block: 19000000

indexer:
batch_size: 50
workers: 10

internal/config/config.go:
package config

import “github.com/spf13/viper”

type Config struct {
Server struct {
Port int
Mode string
}
Postgres struct{ DSN string }
Redis struct{ Addr string }
Ethereum struct {
RPCURL stringmapstructure:"rpc_url"
WSURL stringmapstructure:"ws_url"
StartBlock uint64mapstructure:"start_block"
}
Indexer struct {
BatchSize intmapstructure:"batch_size"
Workers int
}
}

func Load(path string) (*Config, error) {
viper.SetConfigFile(path)
viper.AutomaticEnv()
if err := viper.ReadInConfig(); err != nil {
return nil, err
}
var c Config
return &c, viper.Unmarshal(&c)
}

步骤 3:数据库设计(核心!)

migrations/001_init.sql:
– 区块表
CREATE TABLE blocks (
number BIGINT PRIMARY KEY,
hash VARCHAR(66) UNIQUE NOT NULL,
parent_hash VARCHAR(66) NOT NULL,
timestamp TIMESTAMPTZ NOT NULL,
miner VARCHAR(42) NOT NULL,
gas_used BIGINT,
gas_limit BIGINT,
tx_count INT,
size INT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_blocks_timestamp ON blocks(timestamp DESC);
CREATE INDEX idx_blocks_miner ON blocks(miner);

– 交易表(用分区表抗大数据量)
CREATE TABLE transactions (
hash VARCHAR(66) PRIMARY KEY,
block_number BIGINT NOT NULL,
tx_index INT NOT NULL,
from_addr VARCHAR(42) NOT NULL,
to_addr VARCHAR(42),
value NUMERIC(78,0) NOT NULL, – wei,uint256 最大长度
gas_price NUMERIC(78,0),
gas_used BIGINT,
status SMALLINT, – 1成功 0失败
input_data BYTEA, – 合约调用数据
timestamp TIMESTAMPTZ NOT NULL
);
CREATE INDEX idx_tx_from ON transactions(from_addr, timestamp DESC);
CREATE INDEX idx_tx_to ON transactions(to_addr, timestamp DESC);
CREATE INDEX idx_tx_block ON transactions(block_number);

– 地址账户表(缓存余额等)
CREATE TABLE accounts (
address VARCHAR(42) PRIMARY KEY,
balance NUMERIC(78,0) DEFAULT 0,
nonce BIGINT DEFAULT 0,
is_contract BOOLEAN DEFAULT FALSE,
first_seen TIMESTAMPTZ,
last_seen TIMESTAMPTZ
);

– 合约表
CREATE TABLE contracts (
address VARCHAR(42) PRIMARY KEY REFERENCES accounts(address),
creator VARCHAR(42),
create_tx VARCHAR(66),
bytecode BYTEA,
abi JSONB, – 已验证合约的 ABI
source_code TEXT, – 已验证合约源码
verified BOOLEAN DEFAULT FALSE,
name VARCHAR(100),
created_at TIMESTAMPTZ
);

– 每日统计表
CREATE TABLE daily_stats (
date DATE PRIMARY KEY,
tx_count BIGINT,
block_count INT,
new_addresses INT,
gas_used NUMERIC(78,0),
avg_gas_price NUMERIC(78,0)
);

大白话:4 张主表(区块/交易/账户/合约)+ 1 张统计表。索引建在常查字段上(地址、时间)。NUMERIC(78,0) 是为了存以太坊的
uint256(最大 78 位十进制数)。

步骤 4:领域模型(GORM)

internal/domain/block.go:
package domain

import “time”

type Block struct {
Number uint64gorm:"primaryKey" json:"number"
Hash stringgorm:"size:66;uniqueIndex" json:"hash"
ParentHash stringgorm:"size:66" json:"parent_hash"
Timestamp time.Timejson:"timestamp"
Miner stringgorm:"size:42;index" json:"miner"
GasUsed uint64json:"gas_used"
GasLimit uint64json:"gas_limit"
TxCount intjson:"tx_count"
Size intjson:"size"
}

type Transaction struct {
Hash stringgorm:"primaryKey;size:66" json:"hash"
BlockNumber uint64gorm:"index" json:"block_number"
TxIndex intjson:"tx_index"
From stringgorm:"size:42;index:idx_from_time,priority:1" json:"from"
To *stringgorm:"size:42;index:idx_to_time,priority:1" json:"to"
Value stringgorm:"type:numeric(78,0)" json:"value"
GasPrice stringgorm:"type:numeric(78,0)" json:"gas_price"
GasUsed uint64json:"gas_used"
Status uint8json:"status"
InputData []bytejson:"input_data,omitempty"
Timestamp time.Timegorm:"index:idx_from_time,priority:2;index:idx_to_time,priority:2" json:"timestamp"
}

步骤 5:以太坊节点客户端封装

pkg/ethclient/client.go:
package ethclient

import (
“context”
“math/big”

"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient"

)

type Client struct {
*ethclient.Client
}

func New(rpcURL string) (*Client, error) {
c, err := ethclient.Dial(rpcURL)
if err != nil {
return nil, err
}
return &Client{c}, nil
}

func (c *Client) BlockWithTxs(ctx context.Context, num uint64) (*types.Block, error) {
return c.BlockByNumber(ctx, new(big.Int).SetUint64(num))
}

func (c *Client) TxReceipt(ctx context.Context, hash common.Hash) (*types.Receipt, error) {
return c.TransactionReceipt(ctx, hash)
}

步骤 6:索引器(最核心模块)

internal/indexer/indexer.go:
package indexer

import (
“context”
“log”
“sync”
“time”

"github.com/yourname/chainscan/internal/domain" "github.com/yourname/chainscan/internal/repository" "github.com/yourname/chainscan/pkg/ethclient"

)

type Indexer struct {
eth *ethclient.Client
repo *repository.Repo
batch int
workers int
}

func New(eth *ethclient.Client, repo *repository.Repo, batch, workers int) *Indexer {
return &Indexer{eth: eth, repo: repo, batch: batch, workers: workers}
}

// Run: 从指定区块号开始持续追块
func (i *Indexer) Run(ctx context.Context, startBlock uint64) error {
current := startBlock
for {
latest, err := i.eth.BlockNumber(ctx)
if err != nil {
log.Printf(“get latest block: %v”, err)
time.Sleep(3 * time.Second)
continue
}

if current > latest { time.Sleep(2 * time.Second) // 等新区块 continue } end := current + uint64(i.batch) if end > latest { end = latest } if err := i.processBatch(ctx, current, end); err != nil { log.Printf("batch %d-%d: %v", current, end, err) time.Sleep(3 * time.Second) continue } current = end + 1 }

}

// 并发处理一批区块
func (i *Indexer) processBatch(ctx context.Context, from, to uint64) error {
var wg sync.WaitGroup
sem := make(chan struct{}, i.workers) // 限制并发数
errCh := make(chan error, to-from+1)

for n := from; n <= to; n++ { wg.Add(1) sem <- struct{}{} go func(num uint64) { defer wg.Done() defer func() { <-sem }() if err := i.processBlock(ctx, num); err != nil { errCh <- err } }(n) } wg.Wait() close(errCh) for e := range errCh { if e != nil { return e } } return nil

}

func (i *Indexer) processBlock(ctx context.Context, num uint64) error {
block, err := i.eth.BlockWithTxs(ctx, num)
if err != nil {
return err
}

b := &domain.Block{ Number: block.NumberU64(), Hash: block.Hash().Hex(), ParentHash: block.ParentHash().Hex(), Timestamp: time.Unix(int64(block.Time()), 0), Miner: block.Coinbase().Hex(), GasUsed: block.GasUsed(), GasLimit: block.GasLimit(), TxCount: len(block.Transactions()), Size: int(block.Size()), } txs := make([]domain.Transaction, 0, len(block.Transactions())) for idx, tx := range block.Transactions() { receipt, err := i.eth.TxReceipt(ctx, tx.Hash()) if err != nil { return err } var to *string if tx.To() != nil { s := tx.To().Hex() to = &s } from, _ := i.eth.TransactionSender(ctx, tx, block.Hash(), uint(idx)) txs = append(txs, domain.Transaction{ Hash: tx.Hash().Hex(), BlockNumber: block.NumberU64(), TxIndex: idx, From: from.Hex(), To: to, Value: tx.Value().String(), GasPrice: tx.GasPrice().String(), GasUsed: receipt.GasUsed, Status: uint8(receipt.Status), InputData: tx.Data(), Timestamp: b.Timestamp, }) } return i.repo.SaveBlockWithTxs(ctx, b, txs)

}

大白话:索引器干 3 件事——①每隔几秒查"链上最新区块号";②把"我已经存到的区块"到"最新区块"之间的差距用 10 个 goroutine
并行抓下来;③解析后批量写入 Postgres。

步骤 7:仓储层

internal/repository/repo.go:
package repository

import (
“context”
“github.com/yourname/chainscan/internal/domain”
“gorm.io/gorm”
)

type Repo struct{ db *gorm.DB }

func New(db *gorm.DB) *Repo { return &Repo{db} }

func (r *Repo) SaveBlockWithTxs(ctx context.Context, b *domain.Block, txs []domain.Transaction) error {
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Save(b).Error; err != nil {
return err
}
if len(txs) > 0 {
return tx.CreateInBatches(txs, 100).Error
}
return nil
})
}

func (r *Repo) GetBlock(ctx context.Context, num uint64) (*domain.Block, error) {
var b domain.Block
err := r.db.WithContext(ctx).First(&b, num).Error
return &b, err
}

func (r *Repo) GetTx(ctx context.Context, hash string) (*domain.Transaction, error) {
var t domain.Transaction
err := r.db.WithContext(ctx).First(&t, “hash = ?”, hash).Error
return &t, err
}

func (r *Repo) ListTxByAddress(ctx context.Context, addr string, page, size int) ([]domain.Transaction, int64, error)
{
var txs []domain.Transaction
var total int64
q := r.db.WithContext(ctx).Model(&domain.Transaction{}).
Where(“from_addr = ? OR to_addr = ?”, addr, addr)
q.Count(&total)
err := q.Order(“timestamp DESC”).
Limit(size).Offset((page - 1) * size).
Find(&txs).Error
return txs, total, err
}

步骤 8:服务层 + 缓存

internal/service/block_service.go:
package service

import (
“context”
“encoding/json”
“fmt”
“time”

"github.com/redis/go-redis/v9" "github.com/yourname/chainscan/internal/domain" "github.com/yourname/chainscan/internal/repository"

)

type BlockService struct {
repo *repository.Repo
cache *redis.Client
}

func NewBlockService(r *repository.Repo, c *redis.Client) *BlockService {
return &BlockService{r, c}
}

func (s *BlockService) GetBlock(ctx context.Context, num uint64) (domain.Block, error) {
key := fmt.Sprintf(“block:%d”, num)
if v, err := s.cache.Get(ctx, key).Bytes(); err == nil {
var b domain.Block
_ = json.Unmarshal(v, &b)
return &b, nil
}
b, err := s.repo.GetBlock(ctx, num)
if err != nil {
return nil, err
}
data, _ := json.Marshal(b)
s.cache.Set(ctx, key, data, 10
time.Minute)
return b, nil
}

大白话:先查 Redis,没命中再查 Postgres,结果回写 Redis(10 分钟过期)。这就是经典的 Cache-Aside 模式。

步骤 9:HTTP Handler

internal/handler/block_handler.go:
package handler

import (
“net/http”
“strconv”

"github.com/gin-gonic/gin" "github.com/yourname/chainscan/internal/service"

)

type BlockHandler struct{ svc *service.BlockService }

func NewBlockHandler(s *service.BlockService) *BlockHandler { return &BlockHandler{s} }

// GetBlock godoc
// @Summary 查询区块
// @Tags blocks
// @Param number path int true “区块号”
// @Success 200 {object} domain.Block
// @Router /api/v1/blocks/{number} [get]
func (h *BlockHandler) GetBlock(c *gin.Context) {
num, err := strconv.ParseUint(c.Param(“number”), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{“error”: “invalid block number”})
return
}
b, err := h.svc.GetBlock(c.Request.Context(), num)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{“error”: “not found”})
return
}
c.JSON(http.StatusOK, b)
}

步骤 10:API 入口

cmd/api/main.go:
package main

import (
“log”

"github.com/gin-gonic/gin" "github.com/redis/go-redis/v9" "github.com/yourname/chainscan/internal/config" "github.com/yourname/chainscan/internal/handler" "github.com/yourname/chainscan/internal/repository" "github.com/yourname/chainscan/internal/service" "gorm.io/driver/postgres" "gorm.io/gorm"

)

func main() {
cfg, err := config.Load(“configs/config.yaml”)
if err != nil { log.Fatal(err) }

db, err := gorm.Open(postgres.Open(cfg.Postgres.DSN), &gorm.Config{}) if err != nil { log.Fatal(err) } rdb := redis.NewClient(&redis.Options{Addr: cfg.Redis.Addr}) repo := repository.New(db) blockSvc := service.NewBlockService(repo, rdb) blockH := handler.NewBlockHandler(blockSvc) r := gin.Default() v1 := r.Group("/api/v1") { v1.GET("/blocks/:number", blockH.GetBlock) // v1.GET("/txs/:hash", txH.GetTx) // v1.GET("/addresses/:addr/txs", addrH.ListTxs) // v1.GET("/stats/daily", statsH.Daily) } r.Run(":8080")

}

cmd/indexer/main.go:
package main

import (
“context”
“log”

"github.com/yourname/chainscan/internal/config" "github.com/yourname/chainscan/internal/indexer" "github.com/yourname/chainscan/internal/repository" "github.com/yourname/chainscan/pkg/ethclient" "gorm.io/driver/postgres" "gorm.io/gorm"

)

func main() {
cfg, _ := config.Load(“configs/config.yaml”)
db, _ := gorm.Open(postgres.Open(cfg.Postgres.DSN), &gorm.Config{})
eth, _ := ethclient.New(cfg.Ethereum.RPCURL)
repo := repository.New(db)

idx := indexer.New(eth, repo, cfg.Indexer.BatchSize, cfg.Indexer.Workers) log.Fatal(idx.Run(context.Background(), cfg.Ethereum.StartBlock))

}

步骤 11:统计模块(定时聚合)

// internal/service/stats_service.go
func (s *StatsService) AggregateDaily(ctx context.Context, date time.Time) error {
var stat domain.DailyStat
err := s.db.Raw(SELECT DATE(timestamp) as date, COUNT(*) as tx_count, COUNT(DISTINCT block_number) as block_count, SUM(gas_used::numeric) as gas_used, AVG(gas_price::numeric) as avg_gas_price FROM transactions WHERE DATE(timestamp) = ? GROUP BY DATE(timestamp), date.Format(“2006-01-02”)).Scan(&stat).Error
if err != nil { return err }
return s.db.Save(&stat).Error
}

用 cron 库 robfig/cron/v3 每天 0:05 跑一次。

步骤 12:Docker 一键启动

docker-compose.yml:
version: “3.9”
services:
postgres:
image: postgres:16
environment:
POSTGRES_USER: chainscan
POSTGRES_PASSWORD: secret
POSTGRES_DB: chainscan
ports: [“5432:5432”]
volumes:
- pgdata:/var/lib/postgresql/data
- ./migrations:/docker-entrypoint-initdb.d

redis: image: redis:7-alpine ports: ["6379:6379"] api: build: { context: ., dockerfile: deploy/Dockerfile.api } depends_on: [postgres, redis] ports: ["8080:8080"] indexer: build: { context: ., dockerfile: deploy/Dockerfile.indexer } depends_on: [postgres]

volumes:
pgdata:

deploy/Dockerfile.api:
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o /api ./cmd/api

FROM alpine:3.19
COPY --from=builder /api /api
COPY configs /configs
EXPOSE 8080
CMD [“/api”]

步骤 13:测试

internal/repository/repo_test.go:
func TestSaveBlock(t *testing.T) {
db := setupTestDB(t) // 用 dockertest 起一个 pg 容器
repo := New(db)
b := &domain.Block{Number: 1, Hash: “0xabc”, …}
err := repo.SaveBlockWithTxs(context.Background(), b, nil)
assert.NoError(t, err)

got, _ := repo.GetBlock(context.Background(), 1) assert.Equal(t, "0xabc", got.Hash)

}


四、做"开源项目"的完整流程(很多人忽略的部分)

▎ 写代码只占开源项目工作量的 40%,剩下 60% 是这些。

阶段 A:发布前准备(Day 0)

  1. 选 License
  • MIT(最宽松,公司也能用,推荐 ✅)
  • Apache 2.0(带专利条款)
  • GPL(强传染,慎用)

执行:在仓库根目录建 LICENSE 文件,去 https://choosealicense.com 复制 MIT 模板。

  1. 写好 README.md(决定 80% 的 star)

必须包含:

Chainscan

一句话简介 + 一张架构图

[] [] []

✨ Features

  • ⚡ 毫秒级查询
  • 🔗 兼容所有 EVM 链
  • 📊 内置统计…

🚀 Quick Start

docker-compose up -d
curl localhost:8080/api/v1/blocks/19000000

📖 Documentation

  • Architecture
  • API Reference

🤝 Contributing

See CONTRIBUTING.md

📄 License

MIT

  1. 关键文件

┌──────────────────────────────────┬────────────────────────────────────────────────────────────────────┐
│ 文件 │ 作用 │
├──────────────────────────────────┼────────────────────────────────────────────────────────────────────┤
│ CONTRIBUTING.md │ 怎么提 PR、代码规范 │
├──────────────────────────────────┼────────────────────────────────────────────────────────────────────┤
│ CODE_OF_CONDUCT.md │ 社区行为准则(直接抄 Contributor Covenant) │
├──────────────────────────────────┼────────────────────────────────────────────────────────────────────┤
│ SECURITY.md │ 报告安全漏洞的方式 │
├──────────────────────────────────┼────────────────────────────────────────────────────────────────────┤
│ CHANGELOG.md │ 版本变更日志(遵循 Keep a Changelog (https://keepachangelog.com)) │
├──────────────────────────────────┼────────────────────────────────────────────────────────────────────┤
│ .github/ISSUE_TEMPLATE/bug.md │ Bug 反馈模板 │
├──────────────────────────────────┼────────────────────────────────────────────────────────────────────┤
│ .github/PULL_REQUEST_TEMPLATE.md │ PR 模板 │
└──────────────────────────────────┴────────────────────────────────────────────────────────────────────┘

阶段 B:CI/CD

.github/workflows/ci.yml:
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env: { POSTGRES_PASSWORD: test }
ports: [“5432:5432”]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with: { go-version: ‘1.22’ }
- run: go vet ./…
- run: go test -race -coverprofile=coverage.out ./…
- uses: codecov/codecov-action@v4

lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: golangci/golangci-lint-action@v4

大白话:每次 push 自动跑测试 + 静态检查;PR 没通过 CI 不许合并。

阶段 C:版本发布

用 语义化版本 (Semantic Versioning):v1.2.3 = 主版本.次版本.补丁版本

  • v0.x.x = 开发期,API 可能变
  • v1.0.0 = 正式发布,API 稳定
  • 主版本升 = 不兼容改动
  • 次版本升 = 新功能
  • 补丁版本升 = bug 修复

发布命令:
git tag -a v0.1.0 -m “First release”
git push origin v0.1.0

GitHub Action 自动构建 Docker 镜像 + 生成 release notes

用 goreleaser 自动化(一键生成各平台二进制 + release notes)。

阶段 D:宣传冷启动(最关键,很多项目死在这)

┌─────────────────┬───────────────────────────────────────────────────────────────────────────────┐
│ 渠道 │ 怎么做 │
├─────────────────┼───────────────────────────────────────────────────────────────────────────────┤
│ Hacker News │ “Show HN: Chainscan – a fast block explorer backend”,周二上午 9 点(美东)发 │
├─────────────────┼───────────────────────────────────────────────────────────────────────────────┤
│ Reddit │ r/golang, r/ethereum, r/ethdev │
├─────────────────┼───────────────────────────────────────────────────────────────────────────────┤
│ Twitter/X │ 带 #Ethereum #OpenSource,@相关大V │
├─────────────────┼───────────────────────────────────────────────────────────────────────────────┤
│ 掘金/V2EX/即刻 │ 中文圈 │
├─────────────────┼───────────────────────────────────────────────────────────────────────────────┤
│ Dev.to / Medium │ 写一篇"我为什么造了 Chainscan" │
├─────────────────┼───────────────────────────────────────────────────────────────────────────────┤
│ Awesome List │ 提 PR 到 awesome-go (https://github.com/avelino/awesome-go)、awesome-ethereum │
└─────────────────┴───────────────────────────────────────────────────────────────────────────────┘

阶段 E:长期维护

┌────────────┬─────────────────────────────────┐
│ 任务 │ 频率 │
├────────────┼─────────────────────────────────┤
│ 回复 Issue │ 24 小时内首次响应 │
├────────────┼─────────────────────────────────┤
│ Review PR │ 一周内 │
├────────────┼─────────────────────────────────┤
│ 安全更新 │ dependabot.yml 自动 PR 升级依赖 │
├────────────┼─────────────────────────────────┤
│ 文档更新 │ 跟随代码改动 │
├────────────┼─────────────────────────────────┤
│ 发版 │ 每月一个 minor 版本 │
├────────────┼─────────────────────────────────┤
│ 社区运营 │ 建 Discord/Telegram 群 │
└────────────┴─────────────────────────────────┘

.github/dependabot.yml:
version: 2
updates:
- package-ecosystem: gomod
directory: /
schedule: { interval: weekly }

阶段 F:进阶(项目做大后)

  1. 找 maintainer:积极贡献者邀请进核心组
  2. GOVERNANCE.md:决策机制(BDFL / 委员会)
  3. 加入基金会:CNCF / Apache / Linux Foundation
  4. 商业化:托管服务(SaaS)、企业支持、双 License

五、Makefile(开发者一键命令)

.PHONY: dev test lint build docker

dev:
docker-compose up -d postgres redis
go run ./cmd/indexer & go run ./cmd/api

test:
go test -race -cover ./…

lint:
golangci-lint run

build:
go build -o bin/api ./cmd/api
go build -o bin/indexer ./cmd/indexer

docker:
docker-compose up --build

swagger:
swag init -g cmd/api/main.go -o docs


六、学习路径建议(按顺序做)

  1. Week 1:跑通 docker-compose,能 curl 查到主网真实区块 ✅
  2. Week 2:补全交易/地址/合约查询接口
  3. Week 3:加 Redis 缓存 + 写测试
  4. Week 4:加统计模块 + Swagger 文档
  5. Week 5:CI/CD + 部署到 VPS / Fly.io
  6. Week 6:写 README + 发 HN/掘金,收集第一批用户反馈
  7. 持续:根据 Issue 迭代,6 个月内冲到 1000 star

七、再次回答:“做这个的作用是什么?”

给自己:

  • 一份完整的 Web3 后端作品集 → 简历直接进币安/OKX/Conflux/Polygon
  • 学会 Go + 区块链 + 数据库 + 分布式 + 开源协作(一个项目把后端核心技能全包了)
  • 1000 star 的 GitHub 主页 = 行业敲门砖

给社区:

  • 自建链/L2 团队都需要浏览器(Blockscout 不够轻量),你的项目可能被采用
  • 钱包 App 后端可以直接 fork 你的代码当 API 用

给商业:

  • 同类项目 Etherscan API 年收 5000 万美金,The Graph 估值 6 亿
  • 哪怕只做"专门服务 BSC/Polygon 的轻量浏览器",垂直市场也够养活一家小公司

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询