系统设计实战 179:如何设计一个宠物领养平台

4 阅读10分钟

🚀 系统设计实战 179:如何设计一个宠物领养平台

摘要:每年有数百万流浪动物等待领养,如何用技术让宠物和合适的家庭高效匹配?本文深入讲解智能匹配算法、领养流程状态机、地理位置搜索、回访跟踪系统,并提供完整的 Go 实现。


🎯 场景引入

你想领养一只猫,打开 App:

  • 填写偏好(品种、年龄、性格、住房条件)
  • 系统推荐匹配度最高的宠物,附近 3 家收容所有 5 只猫适合你
  • 提交申请 → 收容所审核 → 视频面试 → 上门家访 → 签约领养
  • 领养后定期回访,确保宠物健康

核心挑战:

  1. 匹配精准度:不是所有人都适合养哈士奇,大型犬需要大房子和运动量
  2. 流程合规:防止虐待动物,需要严格的审核和回访机制
  3. 地理搜索:用户想找附近的收容所和可领养宠物

🎯 场景引入

你打开手机准备使用设计宠物领养平台服务。看似简单的操作背后,系统面临三大核心挑战:

  • 挑战一:高并发——如何在百万级 QPS 下保持低延迟?
  • 挑战二:高可用——如何在节点故障时保证服务不中断?
  • 挑战三:数据一致性——如何在分布式环境下保证数据正确?

🛠️ 需求拆解

1. 功能性需求

  • 宠物管理:收容所录入宠物信息(品种、年龄、性格、健康状况)
  • 智能搜索:按品种/年龄/性格/距离多维筛选
  • 匹配推荐:基于用户画像和宠物特征的智能匹配
  • 领养流程:申请 → 审核 → 面试 → 家访 → 签约 → 回访
  • 回访跟踪:领养后定期回访,健康档案管理
  • 社区互动:领养故事分享、经验交流

2. 非功能性需求

  • 可用性:99.9% 服务可用
  • 搜索延迟:< 200ms
  • 匹配准确率:> 80% 的领养者满意度
  • 数据安全:用户隐私和宠物健康数据加密

3. 容量估算

  • 注册用户:100 万
  • DAU:10 万
  • 在线宠物数:50 万(全国收容所)
  • 日均申请数:5000 笔
  • 搜索 QPS:1000
  • 宠物数据:每条 ~5KB(含图片 URL)→ 总量 ~2.5GB
  • 图片存储:每只宠物 5 张 × 500KB = 2.5MB → 总量 ~1.25TB

推导过程:DAU 1000 万,人均日请求 50 次,日总请求 = 5 亿次

指标数值推导依据
DAU1000 万业务预估
日均请求5 亿DAU × 50 次/人
平均 QPS58005 亿 / 86400 秒
峰值 QPS2.9 万平均 × 5 倍
存储/天~50 GB5 亿 × 100 Bytes

🏗️ 整体架构

┌──────────────────────────────────────────────────────────────┐
│                    客户端                                     │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐                   │
│  │ 移动 App │  │ Web 平台 │  │ 管理后台 │                   │
│  └──────────┘  └──────────┘  └──────────┘                   │
├──────────────────────────────────────────────────────────────┤
│                    API Gateway                               │
├──────────────────────────────────────────────────────────────┤
│                    业务服务层                                 │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐    │
│  │ 宠物服务 │  │ 匹配服务 │  │ 申请服务 │  │ 回访服务 │    │
│  └──────────┘  └──────────┘  └──────────┘  └──────────┘    │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐                   │
│  │ 用户服务 │  │ 通知服务 │  │ 社区服务 │                   │
│  └──────────┘  └──────────┘  └──────────┘                   │
├──────────────────────────────────────────────────────────────┤
│                    存储层                                     │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐    │
│  │ MySQL    │  │ Redis    │  │ ES       │  │ OSS      │    │
│  │(业务数据) │  │(缓存/地理)│  │(全文搜索) │  │(图片存储) │    │
│  └──────────┘  └──────────┘  └──────────┘  └──────────┘    │
└──────────────────────────────────────────────────────────────┘

💡 核心实现

1. 智能匹配服务

// 时间复杂度:O(N),空间复杂度:O(1)

type MatchingService struct {
    petRepo     PetRepository
    userRepo    UserRepository
    geoService  GeoService
    cache       *redis.Client
}

type UserPreference struct {
    Species       string   // dog, cat, rabbit
    Breeds        []string // 偏好品种
    AgeRange      [2]int   // 年龄范围(月)
    Size          string   // small, medium, large
    Personality   []string // 活泼, 安静, 亲人, 独立
    HasChildren   bool     // 家里有小孩
    HasOtherPets  bool     // 家里有其他宠物
    HousingType   string   // apartment, house
    YardSize      string   // none, small, large
    ExperienceLevel string // beginner, intermediate, expert
    MaxDistance   float64  // 最大距离(km)
    Latitude      float64
    Longitude     float64
}

type MatchResult struct {
    Pet        *Pet
    Score      float64 // 0-100 匹配分
    Distance   float64 // 距离(km)
    Highlights []string // 匹配亮点
}

// FindMatches 为用户推荐匹配宠物
func (ms *MatchingService) FindMatches(userID string, pref *UserPreference, limit int) ([]MatchResult, error) {
    // 1. 地理范围筛选(Redis GEO)
    nearbyPetIDs, err := ms.geoService.FindNearbyPets(
        pref.Latitude, pref.Longitude, pref.MaxDistance)
    if err != nil {
        return nil, err
    }

    // 2. 基础条件过滤(ES)
    candidates, err := ms.petRepo.SearchPets(SearchFilter{
        IDs:     nearbyPetIDs,
        Species: pref.Species,
        Status:  "available",
    })
    if err != nil {
        return nil, err
    }

    // 3. 多维度打分
    results := make([]MatchResult, 0, len(candidates))
    for _, pet := range candidates {
        score, highlights := ms.calculateMatchScore(pref, pet)
        dist := ms.geoService.Distance(pref.Latitude, pref.Longitude,
            pet.Shelter.Latitude, pet.Shelter.Longitude)

        results = append(results, MatchResult{
            Pet: pet, Score: score, Distance: dist, Highlights: highlights,
        })
    }

    // 4. 按匹配分排序
    sort.Slice(results, func(i, j int) bool {
        return results[i].Score > results[j].Score
    })

    if len(results) > limit {
        results = results[:limit]
    }
    return results, nil
}

// calculateMatchScore 多维度匹配打分
func (ms *MatchingService) calculateMatchScore(pref *UserPreference, pet *Pet) (float64, []string) {
    score := 0.0
    highlights := []string{}

    // 品种匹配(权重 20%)
    if contains(pref.Breeds, pet.Breed) {
        score += 20
        highlights = append(highlights, "品种匹配")
    }

    // 年龄匹配(权重 15%)
    if pet.AgeMonths >= pref.AgeRange[0] && pet.AgeMonths <= pref.AgeRange[1] {
        score += 15
        highlights = append(highlights, "年龄合适")
    }

    // 体型匹配(权重 15%)
    if pref.Size == pet.Size {
        score += 15
    }

    // 性格匹配(权重 20%)
    personalityMatch := intersectionCount(pref.Personality, pet.PersonalityTraits)
    personalityScore := float64(personalityMatch) / float64(max(len(pref.Personality), 1)) * 20
    score += personalityScore
    if personalityScore > 10 {
        highlights = append(highlights, "性格契合")
    }

    // 居住条件适配(权重 15%)
    score += ms.housingCompatibility(pref, pet)

    // 经验匹配(权重 15%)
    score += ms.experienceCompatibility(pref, pet)

    return score, highlights
}

// housingCompatibility 居住条件适配评分
func (ms *MatchingService) housingCompatibility(pref *UserPreference, pet *Pet) float64 {
    score := 0.0
    // 大型犬需要大院子
    if pet.Size == "large" && pet.Species == "dog" {
        if pref.HousingType == "house" && pref.YardSize == "large" {
            score = 15
        } else if pref.HousingType == "house" {
            score = 8
        } else {
            score = 2 // 公寓养大型犬扣分
        }
    } else {
        score = 12 // 小型宠物对住房要求低
    }
    // 有小孩的家庭避免推荐攻击性品种
    if pref.HasChildren && contains(pet.PersonalityTraits, "aggressive") {
        score = 0
    }
    return score
}

2. 领养流程状态机

// AdoptionState 领养状态
type AdoptionState string

const (
    StateSubmitted   AdoptionState = "submitted"
    StateUnderReview AdoptionState = "under_review"
    StateInterviewScheduled AdoptionState = "interview_scheduled"
    StateHomeVisit   AdoptionState = "home_visit"
    StateApproved    AdoptionState = "approved"
    StateRejected    AdoptionState = "rejected"
    StateCompleted   AdoptionState = "completed"
    StateCancelled   AdoptionState = "cancelled"
)

type AdoptionApplication struct {
    ID            string
    PetID         string
    ApplicantID   string
    State         AdoptionState
    HousingInfo   HousingInfo
    ExperienceInfo ExperienceInfo
    InterviewTime *time.Time
    HomeVisitTime *time.Time
    DecisionReason string
    CreatedAt     time.Time
    UpdatedAt     time.Time
}

// 状态转换规则
var validTransitions = map[AdoptionState][]AdoptionState{
    StateSubmitted:          {StateUnderReview, StateCancelled},
    StateUnderReview:        {StateInterviewScheduled, StateRejected},
    StateInterviewScheduled: {StateHomeVisit, StateRejected, StateCancelled},
    StateHomeVisit:          {StateApproved, StateRejected},
    StateApproved:           {StateCompleted, StateCancelled},
}

type AdoptionService struct {
    appRepo     ApplicationRepository
    petRepo     PetRepository
    notifier    NotificationService
}

// Transition 执行状态转换
func (as *AdoptionService) Transition(appID string, newState AdoptionState, reason string) error {
    app, err := as.appRepo.GetByID(appID)
    if err != nil {
        return err
    }

    // 校验状态转换合法性
    allowed := validTransitions[app.State]
    if !contains(allowed, newState) {
        return fmt.Errorf("非法状态转换: %s → %s", app.State, newState)
    }

    oldState := app.State
    app.State = newState
    app.DecisionReason = reason
    app.UpdatedAt = time.Now()

    if err := as.appRepo.Update(app); err != nil {
        return err
    }

    // 状态变更后的副作用
    switch newState {
    case StateApproved:
        as.petRepo.UpdateStatus(app.PetID, "pending") // 锁定宠物
        as.notifier.SendApprovalNotice(app.ApplicantID, app.PetID)
    case StateCompleted:
        as.petRepo.UpdateStatus(app.PetID, "adopted")
        as.scheduleFollowUps(app) // 安排回访
    case StateRejected:
        as.petRepo.UpdateStatus(app.PetID, "available") // 释放宠物
        as.notifier.SendRejectionNotice(app.ApplicantID, reason)
    }

    log.Printf("领养申请 %s: %s → %s", appID, oldState, newState)
    return nil
}

// scheduleFollowUps 安排领养后回访
func (as *AdoptionService) scheduleFollowUps(app *AdoptionApplication) {
    // 1周、1月、3月、6月、1年 各回访一次
    intervals := []time.Duration{
        7 * 24 * time.Hour,
        30 * 24 * time.Hour,
        90 * 24 * time.Hour,
        180 * 24 * time.Hour,
        365 * 24 * time.Hour,
    }
    for _, interval := range intervals {
        followUp := &FollowUp{
            ID:            generateID(),
            ApplicationID: app.ID,
            PetID:         app.PetID,
            AdopterID:     app.ApplicantID,
            ScheduledAt:   time.Now().Add(interval),
            Status:        "pending",
        }
        as.followUpRepo.Create(followUp)
    }
}

3. 地理搜索(Redis GEO)

type GeoService struct {
    redis *redis.Client
}

const petGeoKey = "pet:geo"

// AddPetLocation 添加宠物位置(收容所坐标)
func (gs *GeoService) AddPetLocation(petID string, lat, lon float64) error {
    return gs.redis.GeoAdd(context.Background(), petGeoKey,
        &redis.GeoLocation{Name: petID, Latitude: lat, Longitude: lon}).Err()
}

// FindNearbyPets 查找附近的宠物
func (gs *GeoService) FindNearbyPets(lat, lon, radiusKm float64) ([]string, error) {
    results, err := gs.redis.GeoRadius(context.Background(), petGeoKey,
        lon, lat, &redis.GeoRadiusQuery{
            Radius: radiusKm, Unit: "km",
            Sort: "ASC", Count: 500,
        }).Result()
    if err != nil {
        return nil, err
    }
    ids := make([]string, len(results))
    for i, r := range results {
        ids[i] = r.Name
    }
    return ids, nil
}

📊 方案对比

维度传统线下领养信息发布平台智能匹配平台(本文)
匹配效率低(靠缘分)中(人工筛选)高(算法推荐)
审核严格度高(面对面)低(无审核)高(全流程管控)
覆盖范围本地全国全国 + 就近推荐
回访跟踪人工自动化
数据沉淀完整用户/宠物画像
退养率~20%~30%~10%(匹配精准)

❓ 高频面试问题

Q1:如何提高匹配准确率?

多维度特征匹配 + 协同过滤。除了显式偏好(品种、年龄),还分析用户浏览行为(停留时间、收藏)。引入"领养成功"的正样本训练模型,持续优化匹配算法。退养数据作为负样本反馈。

Q2:如何防止虐待动物?

严格的审核流程:身份验证 → 居住条件审核 → 视频面试 → 上门家访。领养后定期回访(1周/1月/3月/6月/1年)。建立黑名单机制,有虐待记录的用户永久禁止领养。

Q3:高并发搜索如何优化?

Redis GEO 做地理范围初筛(毫秒级),ES 做全文搜索和多条件过滤,热门宠物列表缓存在 Redis。搜索结果按匹配分排序,分页返回。

Q4:宠物被多人同时申请怎么办?

乐观锁机制:宠物状态为 available 时可接受申请,进入审核后状态变为 pending,其他申请者进入等待队列。如果当前申请被拒绝,自动通知队列中下一位申请者。

Q5:如何设计回访系统?

领养完成后自动创建回访计划(1周/1月/3月/6月/1年)。回访方式:App 内问卷 + 照片上传 + 视频通话。连续两次未回访触发人工介入。回访数据用于优化匹配算法。



🚀 架构演进路径

阶段一:单机版 MVP(用户量 < 10 万)

  • 单体应用 + 单机数据库
  • 功能验证优先,快速迭代
  • 适用场景:产品早期验证

阶段二:基础版分布式(用户量 10 万 - 100 万)

  • 应用层水平扩展(无状态服务 + 负载均衡)
  • 数据库主从分离(读写分离)
  • 引入 Redis 缓存热点数据
  • 适用场景:业务增长期

阶段三:生产级高可用(用户量 > 100 万)

  • 微服务拆分,独立部署和扩缩容
  • 数据库分库分表(按业务维度分片)
  • 引入消息队列解耦异步流程
  • 多机房部署,异地容灾
  • 全链路监控 + 自动化运维

⚖️ 关键 Trade-off 分析

Trade-off 1:一致性 vs 可用性(CP vs AP)

  • 选择 CP:适用于金融、交易等强一致场景,宁可拒绝服务也不能返回错误数据
  • 选择 AP:适用于社交、内容等最终一致场景,优先保证服务可用,允许短暂数据不一致
  • 本系统选择:根据业务场景权衡,核心链路选择 CP,非核心链路选择 AP

Trade-off 2:同步 vs 异步

  • 同步处理:实时性好,但吞吐量受限,适用于用户直接感知的操作
  • 异步处理:吞吐量高,但增加复杂度(消息丢失、重复消费),适用于后台任务
  • 🔴 优缺点:同步简单可靠但扛不住峰值;异步削峰填谷但需要处理最终一致性

✅ 架构设计检查清单

检查项状态
智能匹配✅ 多维度打分 + 协同过滤
地理搜索✅ Redis GEO 就近推荐
流程状态机✅ 严格的状态转换校验
回访跟踪✅ 自动化回访计划
审核机制✅ 多级审核 + 黑名单
通知系统✅ 状态变更实时通知
图片存储✅ OSS + CDN 加速
数据安全✅ 用户隐私加密
缓存策略✅ 热门宠物 + 搜索结果缓存
监控告警✅ 匹配率 / 领养成功率 / 退养率

🔥 故障处理与容灾

故障场景一:节点宕机

  • 现象:请求超时,健康检查失败
  • 应对:自动摘除故障节点 + 流量切换 + 降级策略保证核心功能

故障场景二:数据不一致

  • 现象:主从延迟、缓存与数据库不一致
  • 应对:关键读走主库;Cache-Aside + TTL;异步补偿任务对账修复

故障场景三:下游超时

  • 现象:依赖服务响应变慢,请求堆积
  • 应对:熔断机制(超时率 > 50% 自动熔断);降级返回兜底数据;重试策略(指数退避);回滚到上一个稳定版本

📡 监控与可观测性

关键 Metrics

指标告警阈值说明
P99 延迟> 500ms → Warning核心接口响应时间
错误率> 1% → Critical5xx 错误占比
QPS超过容量 80% → Warning扩容预警

可观测性三支柱

  • Metrics:Prometheus + Grafana 采集系统和业务指标
  • Logging:ELK 集中日志,结构化日志便于检索
  • Tracing:Jaeger 分布式链路追踪,定位跨服务瓶颈