上一篇聊了 PBlack 的架构设计,今天来点更实在的——手把手教你从零搭建一个能跑起来的黑名单检测 API。
读完这篇,你会有:
-
• 一个完整的本地开发环境
-
• 能用的黑名单检测接口
-
• 对核心代码的理解
PBlack 控制台
环境准备
PBlack 的技术栈是 Vue + Django + Go,所以你需要:
# Go 1.21+
go version
# Node.js 18+
node -v
# Python 3.11+
python3 --version
# PostgreSQL 15
# Redis 7
数据库用 Docker 启动最省事:
# docker-compose.yml
version: '3.8'
services:
postgres:
image: postgres:15-alpine
environment:
POSTGRES_DB: pblack
POSTGRES_USER: pblack
POSTGRES_PASSWORD: your_password
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
volumes:
postgres_data:
redis_data:
docker-compose up -d
第一步:搭建 Django 管理后台
Django 负责用户管理、API 密钥、号码库维护。先创建项目:
cd pblack
django-admin startproject padmin
cd padmin
核心模型设计:
# padmin/core/models.py
from django.db import models
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
"""扩展用户模型"""
balance = models.DecimalField(max_digits=12, decimal_places=4, default=0)
price_per_request = models.DecimalField(max_digits=10, decimal_places=4, default=0.01)
class ApiKey(models.Model):
"""API 密钥管理"""
user = models.ForeignKey(User, on_delete=models.CASCADE)
app_id = models.CharField(max_length=32, unique=True)
app_secret = models.CharField(max_length=64)
allowed_ips = models.JSONField(default=list, blank=True)
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
class PhoneBlacklist(models.Model):
"""黑名单号码库"""
phone = models.CharField(max_length=20, db_index=True)
source = models.CharField(max_length=50)
reason = models.CharField(max_length=200, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
indexes = [
models.Index(fields=['phone']),
]
Django Admin 配置:
# padmin/core/admin.py
from django.contrib import admin
from .models import User, ApiKey, PhoneBlacklist
@admin.register(ApiKey)
class ApiKeyAdmin(admin.ModelAdmin):
list_display = ['app_id', 'user', 'is_active', 'created_at']
list_filter = ['is_active', 'created_at']
search_fields = ['app_id', 'user__username']
@admin.register(PhoneBlacklist)
class PhoneBlacklistAdmin(admin.ModelAdmin):
list_display = ['phone', 'source', 'reason', 'created_at']
search_fields = ['phone', 'reason']
list_filter = ['source', 'created_at']
启动管理后台:
python manage.py migrate
python manage.py createsuperuser
python manage.py runserver 0.0.0.0:8000
访问 http://localhost:8000/admin,创建几个测试用的 API 密钥。
API 密钥管理
第二步:核心检测服务(Go)
这是整个系统的性能核心。新建 Go 项目:
mkdir phone-filter-api
cd phone-filter-api
go mod init github.com/yourname/phone-filter-api
项目结构:
phone-filter-api/
├── cmd/
│ └── server/
│ └── main.go
├── internal/
│ ├── api/
│ │ └── handler.go
│ ├── service/
│ │ └── blacklist.go
│ ├── database/
│ │ └── redis.go
│ └── middleware/
│ └── auth.go
├── config/
│ └── config.yaml
└── go.mod
Redis 连接和黑名单查询:
// internal/database/redis.go
package database
import (
"context"
"github.com/redis/go-redis/v9"
)
var rdb *redis.Client
func InitRedis(addr string) {
rdb = redis.NewClient(&redis.Options{
Addr: addr,
DB: 0,
})
}
func IsPhoneInBlacklistSet(ctx context.Context, phone string) (bool, error) {
return rdb.SIsMember(ctx, "blacklist:phones", phone).Result()
}
func AddToBlacklistSet(ctx context.Context, phones ...string) error {
return rdb.SAdd(ctx, "blacklist:phones", phones).Err()
}
核心检测逻辑:
// internal/service/blacklist.go
package service
import (
"context"
"crypto/md5"
"fmt"
"strings"
"time"
"phone-filter-api/internal/database"
)
type CheckRequest struct {
AppID string `json:"appId"`
Timestamp int64 `json:"timestamp"`
Sign string `json:"sign"`
Phone string `json:"phone"`
Level int `json:"level"`
}
type CheckResponse struct {
Code int `json:"code"`
Message string `json:"message"`
IsBlocked bool `json:"isBlocked"`
}
func (s *BlacklistService) Check(ctx context.Context, req *CheckRequest) (*CheckResponse, error) {
// 1. 验证签名
if !s.verifySign(req) {
return &CheckResponse{Code: 401, Message: "invalid sign"}, nil
}
// 2. 检查时间戳(5分钟有效期)
if time.Now().Unix()-req.Timestamp > 300 {
return &CheckResponse{Code: 401, Message: "timestamp expired"}, nil
}
// 3. 标准化手机号
phone := normalizePhone(req.Phone)
// 4. 执行检测
isBlocked, _ := database.IsPhoneInBlacklistSet(ctx, phone)
// 5. 异步记录访问日志
go s.logAccess(req.AppID, phone, isBlocked)
return &CheckResponse{
Code: 200,
Message: "success",
IsBlocked: isBlocked,
}, nil
}
func normalizePhone(phone string) string {
var result strings.Builder
for _, c := range phone {
if c >= '0' && c <= '9' {
result.WriteRune(c)
}
}
return result.String()
}
启动服务:
// cmd/server/main.go
package main
import (
"log"
"net/http"
"phone-filter-api/internal/api"
"phone-filter-api/internal/database"
)
func main() {
database.InitRedis("localhost:6379")
handler := api.NewHandler()
http.HandleFunc("/api/v1/check", handler.CheckPhone)
log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
运行:
go mod tidy
go run cmd/server/main.go
第三步:测试接口
先用 Redis CLI 导入几条测试数据:
redis-cli
SADD blacklist:phones "13800138000" "13900139000" "15000150000"
然后用 curl 测试:
# 生成签名(假设 appId=test001, appSecret=secret123)
TIMESTAMP=$(date +%s)
SIGN=$(echo -n "test001${TIMESTAMP}secret123" | md5sum | cut -d' ' -f1)
curl -X POST http://localhost:8080/api/v1/check \
-H "Content-Type: application/json" \
-d "{
\"appId\": \"test001\",
\"timestamp\": $TIMESTAMP,
\"sign\": \"$SIGN\",
\"phone\": \"13800138000\",
\"level\": 1
}"
预期返回:
{
"code": 200,
"message": "success",
"isBlocked": true
}
测试一个不在黑名单的号码:
{
"code": 200,
"message": "success",
"isBlocked": false
}
第四步:批量导入黑名单
实际项目中,黑名单可能来自多个渠道:
// internal/service/importer.go
package service
import (
"bufio"
"context"
"os"
"strings"
"phone-filter-api/internal/database"
)
func ImportFromFile(ctx context.Context, filepath string, source string) (int, error) {
file, err := os.Open(filepath)
if err != nil {
return 0, err
}
defer file.Close()
var phones []string
scanner := bufio.NewScanner(file)
for scanner.Scan() {
phone := strings.TrimSpace(scanner.Text())
if phone != "" {
phones = append(phones, normalizePhone(phone))
}
}
// 批量写入 Redis
if err := database.AddToBlacklistSet(ctx, phones...); err != nil {
return 0, err
}
return len(phones), nil
}
导入命令:
go run cmd/import/main.go --file=blacklist.txt --source=manual
第五步:性能压测
写个简单的压测脚本:
#!/bin/bash
# benchmark.sh
CONCURRENT=100
REQUESTS=10000
API_URL="http://localhost:8080/api/v1/check"
TIMESTAMP=$(date +%s)
SIGN=$(echo -n "test001${TIMESTAMP}secret123" | md5sum | cut -d' ' -f1)
ab -n $REQUESTS -c $CONCURRENT \
-T "application/json" \
-p <(echo "{
\"appId\": \"test001\",
\"timestamp\": $TIMESTAMP,
\"sign\": \"$SIGN\",
\"phone\": \"13800138000\",
\"level\": 1
}") \
$API_URL
在我的笔记本(M1 Pro)上实测数据:
| 指标 | 数值 | | --- | --- | | 单机 QPS | 8,500+ | | 平均响应 | 5ms | | P99 延迟 | 12ms | | Redis 命中率 | 100% |
这个性能对于中小业务完全够用了。如果需要更高并发,水平扩展几台机器就行。
常见问题
Q: 黑名单数据怎么同步到 Redis?
A: 几种方案:
Q: 怎么防止 API 密钥泄露?
A: 除了签名验证,建议加上:
-
• IP 白名单限制
-
• 请求频率限制(rate limiting)
-
• 异常调用监控告警
Q: 需要支持国际号码吗?
A: 目前代码只处理了数字提取。如果需要支持国际号码(如 +86-138-0013-8000),可以在 normalizePhone 函数里保留 + 号,或者增加国家码字段。
总结
这篇文章带你从零搭建了一个可用的手机号黑名单检测 API。核心要点:
代码已经能跑起来了,下一篇我会聊聊如何部署到生产环境,包括 Nginx 配置、systemd 服务托管、监控告警这些运维话题。