区块浏览器后端:区块/交易/地址/合约查询、链数据统计.
我直接用大白话给你讲清楚一个区块浏览器后端从 0 到开源完整维护的全流程含可运行代码。一、先回答做这个有什么用区块浏览器是什么你用过 Etherscan、BscScan、Solscan 吧输入一个交易哈希能看到这笔交易转了多少钱、给谁、什么时候、Gas多少输入一个地址能看到余额、所有历史交易输入一个合约能看到代码、调用次数。这就是区块浏览器。区块链节点本身比如 Geth只存原始数据查询效率极低你想查这个地址近 30天所有交易节点得把全网数据扫一遍慢到无法用。区块浏览器后端的作用就是把链上数据整理 索引 缓存到普通数据库里让查询从几分钟变成几毫秒。做这个项目的 5 大作用真实价值┌────────────┬─────────────────────────────────────────────────────────────────────────────┐│ 作用 │ 大白话解释 │├────────────┼─────────────────────────────────────────────────────────────────────────────┤│ 数据可读化 │ 链上数据是十六进制乱码普通人看不懂你把它翻译成Alice 转 1.5 ETH 给 Bob │├────────────┼─────────────────────────────────────────────────────────────────────────────┤│ 查询加速 │ 节点查某地址历史交易要扫全链你建好索引后 5ms 返回 │├────────────┼─────────────────────────────────────────────────────────────────────────────┤│ 业务支撑 │ 钱包 App、交易所、DeFi 项目都需要这种 API自己跑节点太贵 │├────────────┼─────────────────────────────────────────────────────────────────────────────┤│ 数据分析 │ 链上活跃度、Gas 趋势、合约调用排行——这些都靠你聚合 │├────────────┼─────────────────────────────────────────────────────────────────────────────┤│ 简历加分 │ 这是 Web3 后端工程师的标准项目写完简历能直接进交易所/钱包公司 │└────────────┴─────────────────────────────────────────────────────────────────────────────┘谁在用类似系统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 chainscango mod init github.com/yourname/chainscangit 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.yamlserver:port: 8080mode: debugpostgres:dsn: “hostlocalhost userchainscan passwordsecret dbnamechainscan port5432 sslmodedisable”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: 19000000indexer:batch_size: 50workers: 10internal/config/config.gopackage configimport “github.com/spf13/viper”type Config struct {Server struct {Port intMode string}Postgres struct{ DSN string }Redis struct{ Addr string }Ethereum struct {RPCURL stringmapstructure:rpc_urlWSURL stringmapstructure:ws_urlStartBlock uint64mapstructure:start_block}Indexer struct {BatchSize intmapstructure:batch_sizeWorkers int}}func Load(path string) (*Config, error) {viper.SetConfigFile(path)viper.AutomaticEnv()if err : viper.ReadInConfig(); err ! nil {return nil, err}var c Configreturn 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, – 已验证合约的 ABIsource_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领域模型GORMinternal/domain/block.gopackage domainimport “time”type Block struct {Number uint64gorm:primaryKey json:numberHash stringgorm:size:66;uniqueIndex json:hashParentHash stringgorm:size:66 json:parent_hashTimestamp time.Timejson:timestampMiner stringgorm:size:42;index json:minerGasUsed uint64json:gas_usedGasLimit uint64json:gas_limitTxCount intjson:tx_countSize intjson:size}type Transaction struct {Hash stringgorm:primaryKey;size:66 json:hashBlockNumber uint64gorm:index json:block_numberTxIndex intjson:tx_indexFrom stringgorm:size:42;index:idx_from_time,priority:1 json:fromTo *stringgorm:size:42;index:idx_to_time,priority:1 json:toValue stringgorm:type:numeric(78,0) json:valueGasPrice stringgorm:type:numeric(78,0) json:gas_priceGasUsed uint64json:gas_usedStatus uint8json:statusInputData []bytejson:input_data,omitemptyTimestamp time.Timegorm:index:idx_from_time,priority:2;index:idx_to_time,priority:2 json:timestamp}步骤 5以太坊节点客户端封装pkg/ethclient/client.gopackage ethclientimport (“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.gopackage indexerimport (“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.Clientrepo *repository.Repobatch intworkers 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 : startBlockfor {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.WaitGroupsem : make(chan struct{}, i.workers) // 限制并发数errCh : make(chan error, to-from1)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.gopackage repositoryimport (“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.Blockerr : r.db.WithContext(ctx).First(b, num).Errorreturn b, err}func (r *Repo) GetTx(ctx context.Context, hash string) (*domain.Transaction, error) {var t domain.Transactionerr : r.db.WithContext(ctx).First(t, “hash ?”, hash).Errorreturn t, err}func (r *Repo) ListTxByAddress(ctx context.Context, addr string, page, size int) ([]domain.Transaction, int64, error){var txs []domain.Transactionvar total int64q : 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).Errorreturn txs, total, err}步骤 8服务层 缓存internal/service/block_service.gopackage serviceimport (“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.Repocache *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, 10time.Minute)return b, nil}大白话先查 Redis没命中再查 Postgres结果回写 Redis10 分钟过期。这就是经典的 Cache-Aside 模式。步骤 9HTTP Handlerinternal/handler/block_handler.gopackage handlerimport (“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)}步骤 10API 入口cmd/api/main.gopackage mainimport (“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.gopackage mainimport (“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.gofunc (s *StatsService) AggregateDaily(ctx context.Context, date time.Time) error {var stat domain.DailyStaterr : 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).Errorif err ! nil { return err }return s.db.Save(stat).Error}用 cron 库 robfig/cron/v3 每天 0:05 跑一次。步骤 12Docker 一键启动docker-compose.ymlversion: “3.9”services:postgres:image: postgres:16environment:POSTGRES_USER: chainscanPOSTGRES_PASSWORD: secretPOSTGRES_DB: chainscanports: [“5432:5432”]volumes:- pgdata:/var/lib/postgresql/data- ./migrations:/docker-entrypoint-initdb.dredis: 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.apiFROM golang:1.22-alpine AS builderWORKDIR /appCOPY go.mod go.sum ./RUN go mod downloadCOPY . .RUN go build -o /api ./cmd/apiFROM alpine:3.19COPY --frombuilder /api /apiCOPY configs /configsEXPOSE 8080CMD [“/api”]步骤 13测试internal/repository/repo_test.gofunc 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选 LicenseMIT最宽松公司也能用推荐 ✅Apache 2.0带专利条款GPL强传染慎用执行在仓库根目录建 LICENSE 文件去 https://choosealicense.com 复制 MIT 模板。写好 README.md决定 80% 的 star必须包含Chainscan一句话简介 一张架构图[] [] []✨ Features⚡ 毫秒级查询 兼容所有 EVM 链 内置统计… Quick Startdocker-compose up -dcurl localhost:8080/api/v1/blocks/19000000 DocumentationArchitectureAPI Reference ContributingSee CONTRIBUTING.md LicenseMIT关键文件┌──────────────────────────────────┬────────────────────────────────────────────────────────────────────┐│ 文件 │ 作用 │├──────────────────────────────────┼────────────────────────────────────────────────────────────────────┤│ 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 模板 │└──────────────────────────────────┴────────────────────────────────────────────────────────────────────┘阶段 BCI/CD.github/workflows/ci.ymlname: CIon: [push, pull_request]jobs:test:runs-on: ubuntu-latestservices:postgres:image: postgres:16env: { POSTGRES_PASSWORD: test }ports: [“5432:5432”]steps:- uses: actions/checkoutv4- uses: actions/setup-gov5with: { go-version: ‘1.22’ }- run: go vet ./…- run: go test -race -coverprofilecoverage.out ./…- uses: codecov/codecov-actionv4lint: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - uses: golangci/golangci-lint-actionv4大白话每次 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.0GitHub 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.ymlversion: 2updates:- package-ecosystem: gomoddirectory: /schedule: { interval: weekly }阶段 F进阶项目做大后找 maintainer积极贡献者邀请进核心组GOVERNANCE.md决策机制BDFL / 委员会加入基金会CNCF / Apache / Linux Foundation商业化托管服务SaaS、企业支持、双 License五、Makefile开发者一键命令.PHONY: dev test lint build dockerdev:docker-compose up -d postgres redisgo run ./cmd/indexer go run ./cmd/apitest:go test -race -cover ./…lint:golangci-lint runbuild:go build -o bin/api ./cmd/apigo build -o bin/indexer ./cmd/indexerdocker:docker-compose up --buildswagger:swag init -g cmd/api/main.go -o docs六、学习路径建议按顺序做Week 1跑通 docker-compose能 curl 查到主网真实区块 ✅Week 2补全交易/地址/合约查询接口Week 3加 Redis 缓存 写测试Week 4加统计模块 Swagger 文档Week 5CI/CD 部署到 VPS / Fly.ioWeek 6写 README 发 HN/掘金收集第一批用户反馈持续根据 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 的轻量浏览器垂直市场也够养活一家小公司