云养鸟系统:架构设计与实现
引言
本文将系统性地介绍我们云养鸟项目的技术架构设计、双系统融合方案、核心功能实现,以及开发过程中的技术挑战与解决方案。这是一个同时支持面向管理员的桌面端鸟场管理系统,和面向普通用户的移动端云养系统的双系统项目,我们通过路由层隔离 + 统一认证的融合方案,在单一域名下实现了两套系统的并行部署,同时解决了权限隔离、样式冲突、状态污染等一系列问题。
一、双系统融合架构设计
项目面临的核心挑战在于:如何在同一域名下同时支持面向管理员的桌面端鸟场管理系统和面向普通用户的移动端云养系统。传统方案通常选择拆分部署或使用子域名,但经过技术评估,我们选择了路由层隔离 + 统一认证的融合方案。
1.1 架构设计理念
这一选择基于以下几个维度的考量:
-
用户体验维度:单一域名降低用户记忆成本,用户无需区分访问入口,系统根据用户角色自动引导至对应界面。管理员与普通用户共享登录入口,简化账号管理流程。
-
运维成本维度:统一部署单元减少服务器配置复杂度,无需维护多套 SSL 证书和域名解析规则。
-
安全管控维度:集中化的认证网关便于实施统一的访问控制策略,权限验证逻辑集中管理,降低安全漏洞风险。
1.2 路由策略实现
前端采用 Vue Router 进行路由管理,通过路径前缀实现双系统的路由隔离:
// 鸟场管理系统路由(桌面端)const managementRoutes = [ { path: '/Home', component: Home }, { path: '/Stream', component: Stream }, { path: '/Device', component: Device }, { path: '/Data', component: Data }, { path: '/Profile', component: Profile }, { path: '/UserManage', component: UserManage }, { path: '/birds', component: BirdsPage }];// 云养鸟系统路由(移动端),统一前缀 /cloudconst cloudRoutes = { path: '/cloud', component: CloudApp, children: [ { path: 'login', component: () => import('@/cloud/pages/LoginPage.vue') }, { path: 'register', component: () => import('@/cloud/pages/RegisterPage.vue') }, { path: 'home', component: () => import('@/cloud/pages/CloudHomePage.vue') }, { path: 'birds', component: () => import('@/cloud/pages/BirdsPage.vue') }, { path: 'adopt-birds', component: () => import('@/cloud/pages/AdoptBirdsPage.vue') }, { path: 'my', component: () => import('@/cloud/pages/MyPage.vue') }, { path: 'bird/:id', component: () => import('@/cloud/pages/BirdDetailPage.vue') } ]};路由守卫在入口层面实施权限控制,根据用户角色决定访问权限:
router.beforeEach((to, from) => { const userRole = localStorage.getItem('role') || ''; const isCloudRoute = to.path.startsWith('/cloud'); const isclouduser = userRole === 'clouduser'; // clouduser 角色权限控制 if (isclouduser) { if (isCloudRoute) { return true; // 允许访问云养系统 } else { alert('权限不足,您只能访问云养系统页面'); return '/cloud/my'; // 强制跳转云养系统 } } // 非云养路由需要登录验证 if (!isCloudRoute && to.path !== '/Login' && !isAuthenticated) { return '/Login'; }});1.3 资源隔离机制
双系统在资源层面实现了有效的物理隔离,避免相互干扰:
- 组件隔离:云养系统使用独立的 Vue 应用实例
CloudApp,拥有独立的组件树和状态管理。这避免了样式冲突和状态污染。
<!-- 独立应用容器 --><template> <div class="cloud-app"> <RouterView /> <BottomNav /> <!-- 移动端专属底部导航 --> </div></template>- 样式隔离:通过 Vue 的 Scoped CSS 机制和独立的样式文件,确保移动端组件样式不会影响桌面端。同时,移动端组件内部使用媒体查询限制样式作用范围。
// 桌面端提示遮罩,仅在大屏时显示@media (min-width: 769px) { .desktop-notice { display: flex; position: fixed; z-index: 9999; }}- 构建产物隔离:虽然最终打包在同一应用中,但通过动态导入实现代码分割,各路由组件按需加载,不会互相影响。
1.4 API 代理配置
统一域名下的 API 代理配置确保前后端通信的顺畅,同时支持 WebSocket 代理:
server: { proxy: { '/api': { target: 'https://lark.mintlab.top', changeOrigin: true, secure: false, ws: true // 支持 WebSocket 代理 }, '/stream': { target: 'https://lark.mintlab.top', changeOrigin: true, ws: true } }}1.5 架构优势与挑战
技术优势:
-
统一体验:用户通过单一入口访问,降低认知负担
-
便捷运维:集中化部署减少服务器资源占用和配置复杂度
-
代码复用:共享的 API 工具类和认证逻辑减少重复代码
-
灵活扩展:新增功能只需扩展路由配置,无需调整服务器架构
潜在挑战与解决方案:
| 挑战 | 解决方案 |
|---|---|
| 移动端与桌面端样式相互干扰 | 采用独立的样式命名空间和 Scoped CSS |
| 角色切换需要重新登录 | 实现 Token 自动刷新机制,支持无感知权限变更 |
| 单点登录后重复验证 | 引入请求队列管理,避免并发 Token 刷新 |
| 移动端在桌面端误访问 | 添加设备检测和友好提示界面 |
二、数据库架构设计
2.1 核心数据模型
系统采用 SQLAlchemy ORM 进行数据库建模,核心数据模型包括用户、设备、传感器和雏鸟四大实体。
用户模型扩展:系统在标准用户表基础上通过 extra JSON 字段存储用户额外信息,支持灵活扩展用户属性:
class M_Users(Base): __tablename__ = 'users' id = Column(Integer, primary_key=True) username = Column(String(50), unique=True, nullable=False) password = Column(String(255), nullable=False) role = Column(String(20), default='user') # root/clouduser/user extra = Column(JSON, default=dict) # 存储已认领雏鸟等信息雏鸟模型设计:雏鸟表存储鸟类基本信息及关联设备,支持状态流转:
class Birds(Base): __tablename__ = 'birds' id = Column(Integer, primary_key=True) name = Column(String(100), nullable=False) # 雏鸟名称 species = Column(String(100)) # 品种 birth_date = Column(Date) # 出生日期 status = Column(String(20), default='available') # available/adopted device_id = Column(Integer, ForeignKey('devices.id')) # 关联设备 user_id = Column(Integer, ForeignKey('users.id'), nullable=True) # 认领用户邀请码模型:支持指定用户类型的邀请码机制,用于区分注册用户的角色:
class InvitationCodes(Base): __tablename__ = 'invitation_codes' id = Column(Integer, primary_key=True) code = Column(String(20), unique=True) user_type = Column(String(20), default='clouduser') # 指定注册用户的类型 created_by = Column(Integer, ForeignKey('users.id')) created_at = Column(DateTime, default=datetime.utcnow)2.2 SQLAlchemy 陷阱与经验
在开发过程中,我遇到了 SQLAlchemy 对 JSON 字段变更追踪失效的问题。表现为修改 user\.extra 字典后数据库未正确更新。
问题根因:SQLAlchemy 对嵌套对象的原地修改无法自动建立变更追踪。
解决方案:使用 dict\(\) 创建字典副本后赋值,强制 SQLAlchemy 建立变更追踪:
# 错误写法(不会触发更新)user.extra['adopted_bird_id'] = bird_id# 正确写法extra = dict(user.extra) if user.extra else {}extra['adopted_bird_id'] = bird_iduser.extra = extra这个方案解决了我们在用户认领、释放雏鸟等多个场景下的更新问题。
三、RESTful API 设计
3.1 API 架构规范
后端采用 FastAPI 构建 RESTful API,遵循资源导向的 URL 设计和标准 HTTP 方法语义。
用户管理 API:
# 用户注册 - 根据邀请码自动分配角色POST /api/users/register{ "username": "user1", "password": "***", "invitation_code": "ABC123" # 邀请码包含 user_type}# 获取当前用户已认领雏鸟GET /api/users/birds/adopted/me# 释放当前用户已认领雏鸟DELETE /api/users/birds/adopted/me# 查找指定雏鸟的认领用户GET /api/users/birds/adopted/{bird_id}# 管理员强制释放用户已认领雏鸟DELETE /api/users/{user_id}/birds/adopted雏鸟管理 API:
# CRUD 操作POST /api/birds # 创建雏鸟(仅 root)GET /api/birds # 查询雏鸟列表GET /api/birds/{id} # 查询单个雏鸟PUT /api/birds/{id} # 更新雏鸟(仅 root)DELETE /api/birds/{id} # 删除雏鸟(仅 root)# 认领操作POST /api/birds/{bird_id}/adopt # 认领雏鸟DELETE /api/birds/adopted/me # 释放已认领雏鸟3.2 权限控制实现
API 层通过依赖注入实现细粒度的权限控制:
from fastapi import Depends, HTTPExceptionasync def require_root(current_user: M_Users = Depends(GetCurrentUser)): if current_user.role != 'root': raise HTTPException(status_code=403, detail="需要管理员权限") return current_user
@router.post('/birds', response_model=CommonOut[Bird])async def create_bird( bird: BirdCreate, current_user: M_Users = Depends(require_root) # 仅 root 可创建): # 创建逻辑 pass通过这种方式,我们可以为每个接口灵活配置权限,保证不同角色的用户只能访问自己有权限的接口。
四、前端架构设计
4.1 移动端应用架构
云养鸟移动端采用独立 Vue 应用架构,通过 CloudApp\.vue 作为容器组件:
<template> <div class="cloud-app"> <!-- 桌面端提示遮罩 --> <div class="desktop-notice"> <p>请使用移动端访问此网页</p> </div> <!-- 页面内容,带过渡动画 --> <transition name="page-transition" mode="out-in"> <RouterView :key="$route.fullPath" /> </transition> <!-- 底部导航栏 --> <BottomNav /> </div></template>这个容器组件实现了桌面端访问的提示,同时提供了页面切换的过渡动画,以及移动端专属的底部导航。
4.2 Token 刷新机制
前端实现智能的 Token 刷新机制,处理认证失效场景,同时解决并发刷新的问题:
// 请求队列管理,避免并发刷新let isRefreshing = false;let requestQueue: Array<(token: string) => void> = [];
// 处理 401 响应async function handleUnauthorized(): Promise<string> { if (isRefreshing) { // 排队等待新 Token return new Promise(resolve => { requestQueue.push(resolve); }); } isRefreshing = true;
try { const newToken = await refreshToken(); if (newToken) { // 通知排队请求使用新 Token requestQueue.forEach(resolve => resolve(newToken)); return newToken; } } catch { // 刷新失败,跳转登录 redirectToLogin(); } finally { isRefreshing = false; requestQueue = []; } throw new Error('Token refresh failed');}这个机制保证了当多个请求同时遇到 401 时,只会触发一次 Token 刷新,其他请求会排队等待,避免了重复刷新的问题。
4.3 组件设计模式
移动端页面采用组合式组件设计,分离关注点:
<!-- 页面级别的 BirdsPage.vue --><template> <div class="birds-page"> <Tabs v-model:active="activeTab" @update:active="handleTabChange" /> <BirdList :birds="displayedBirds" @select="handleSelect" /> <BottomSheet v-if="selectedBird" :bird="selectedBird" /> </div></template>
<!-- 可复用的底部导航栏 BottomNav.vue --><template> <nav class="bottom-nav"> <NavItem v-for="item in navItems" :key="item.path" :item="item" /> </nav></template>我们将页面拆分为多个可复用的小组件,每个组件负责单一的功能,提高了代码的可维护性和复用性。
五、核心功能实现
5.1 用户认证与注册
用户注册流程与邀请码机制深度集成,邀请码会自动决定用户的角色:
@router.post("/register", response_model=CommonOut)async def register_user(user: UserRegister, db: Session = Depends(GetDb)): # 验证邀请码 invitation = db.query(InvitationCodes).filter_by(code=user.invitation_code).first() if not invitation: raise HTTPException(status_code=400, detail="无效的邀请码")
# 根据邀请码决定用户角色 user_role = invitation.user_type if invitation.user_type else 'clouduser' # 创建用户 new_user = M_Users( username=user.username, password=hash_password(user.password), role=user_role ) db.add(new_user) db.commit() return CommonOut(data={"user_id": new_user.id, "role": new_user.role})通过这种方式,我们可以通过邀请码,自动区分用户是普通管理员还是云养用户,无需用户手动选择。
5.2 雏鸟认领逻辑
雏鸟认领实现一对一绑定约束,保证每个用户只能认领一只雏鸟:
@router.post("/{bird_id}/adopt", response_model=CommonOut)async def adopt_bird( bird_id: int, current_user: M_Users = Depends(GetCurrentUser), db: Session = Depends(GetDb)): # 检查用户是否已有认领的雏鸟 if current_user.extra and current_user.extra.get('adopted_bird_id'): raise HTTPException(status_code=400, detail="您已认领一只雏鸟")
# 检查雏鸟是否可被认领 bird = db.query(Birds).filter_by(id=bird_id).first() if bird.status != 'available': raise HTTPException(status_code=400, detail="该雏鸟已被认领")
# 执行认领 bird.status = 'adopted' bird.user_id = current_user.id # 更新用户额外信息 extra = dict(current_user.extra) if current_user.extra else {} extra['adopted_bird_id'] = bird_id current_user.extra = extra
db.commit() return CommonOut(message="认领成功")这个逻辑保证了认领的原子性,同时同步更新了雏鸟和用户的状态。
5.3 账号注销与资源释放
账号注销时自动释放已认领资源,保证数据的一致性:
@router.delete("/{user_id}", response_model=CommonOut)async def delete_user( user_id: int, current_user: M_Users = Depends(GetCurrentUser), db: Session = Depends(GetDb)): # 仅 root 或本人可删除 if current_user.role != 'root' and current_user.id != user_id: raise HTTPException(status_code=403, detail="权限不足")
target_user = db.query(M_Users).filter_by(id=user_id).first() # 自动释放已认领雏鸟 if target_user.extra and target_user.extra.get('adopted_bird_id'): ReleaseAdoptedBird(db, user_id)
db.delete(target_user) db.commit() return CommonOut(message="注销成功")当用户注销账号时,我们会自动释放他已认领的雏鸟,让这只雏鸟可以被其他用户重新认领,保证了数据的一致性。
六、开发经验与总结
6.1 关键设计模式
我们在项目中应用了这些设计模式,提升了代码的可维护性:
-
策略模式在权限控制中的应用:通过依赖注入实现不同角色的权限验证策略,便于扩展和维护。
-
观察者模式在状态管理中的应用:Pinia 状态管理采用响应式更新机制,组件自动响应状态变化。
-
工厂模式在 API 请求中的应用:统一的 API 请求工厂封装认证逻辑、错误处理和响应转换。
6.2 性能优化策略
我们在项目中做了这些性能优化: 前端优化:
-
路由级代码分割,按需加载页面组件
-
图片资源懒加载,减少首屏加载时间
-
WebSocket 连接复用,避免频繁建立连接
后端优化:
-
数据库索引优化,常用查询字段建立索引
-
连接池管理,合理配置 SQLAlchemy 连接池参数
-
缓存策略,对不常变化的配置数据实施内存缓存
6.3 安全实践经验
我们也做了这些安全实践,保证系统的安全: 认证安全:
-
密码哈希存储,使用 bcrypt 等成熟算法
-
Token 过期机制,定期轮换访问令牌
-
请求频率限制,防止暴力破解
数据安全:
-
SQL 注入防护,使用参数化查询
-
CORS 配置,限制跨域请求
-
敏感信息脱敏,日志中不输出敏感字段
部分信息可能已经过时









