简介:这是一套面向全栈开发者的现代化后台管理系统源码,适用于希望掌握前后端分离架构、跨端开发与企业级权限管理的中高级开发者。系统采用FastAPI构建高性能异步后端,支持RBAC权限控制、定时任务与部门管理;前端分为PC端(Vue3 + TypeScript + Vite + Element Plus)和微信小程序端(Uni-APP + uView),实现业务逻辑复用与多端适配。资源包共924个文件,含210个Vue组件、168个Python脚本(含FastAPI路由与数据库模型)、158个TypeScript文件(含状态管理与接口定义)、87个JSON配置及46篇Markdown文档(含部署说明与API规范),整体压缩包仅12.2MB,结构清晰、开箱即用。内容预览可见redis.conf、nginx.conf、Dockerfile及多种UI样式文件,体现完整DevOps支持能力。目前已有336人学习下载,适合用于二次开发、教学演示或快速搭建企业级管理平台原型。
1. 为什么 FastAPI + Vue3 + 微信小程序三端共用一套后台,正在成为中型业务系统的事实标准?
很多团队还在为「PC 管理后台用 Vue2、小程序自己写一套、接口又得额外适配」疲于奔命——结果是三套逻辑、两套鉴权、一个 bug 要修三次。而真正跑通的项目已经把「FastAPI 后端统一提供 RESTful 接口 + Vue3 PC 端管理界面 + 微信小程序轻量前台」做成标准交付模板:PC 端走完整权限控制与数据看板,小程序只暴露用户侧高频操作(如扫码核销、工单提交、状态查询),两者共享同一套/api/v1/接口层、同一套 RBAC 权限模型、同一套 Redis 缓存策略。这不是理想主义,而是基于真实压测数据的选择:FastAPI 在并发 3000+ 请求下平均响应 < 42ms(对比 Django 同配置 118ms),Vue3 的<script setup>+defineModel让表单开发效率提升 40%,微信小程序原生渲染能力已支持 WebSocket 实时通知与本地缓存穿透。适合需要快速迭代、多角色协同、且对首屏加载和接口吞吐有明确 SLA 要求的 SaaS 工具类、本地生活服务、设备运维平台等场景。
2. FastAPI 后端设计:从路由分层到 Redis ACL 鉴权落地
2.1 路由结构必须按终端能力切分,而非按业务模块硬拆
常见误区是把所有接口塞进app/api/v1/items.py,结果小程序调用/api/v1/items/list却返回了 PC 端才需要的created_by_name字段,徒增带宽与解析开销。正确做法是按终端能力声明路由前缀:
# app/main.py from fastapi import FastAPI from app.api.pc import router as pc_router from app.api.mp import router as mp_router app = FastAPI(title="Admin & MP Backend", version="1.2.0") # PC 端专用路由(含完整字段、导出、审计日志) app.include_router(pc_router, prefix="/api/v1/pc", tags=["PC Admin"]) # 小程序端专用路由(精简字段、无敏感字段、强制 token 绑定 openid) app.include_router(mp_router, prefix="/api/v1/mp", tags=["WeChat MiniProgram"])提示:
/api/v1/pc和/api/v1/mp是物理隔离的路由空间,即使同名 endpoint(如GET /items)也必须在各自 router 文件中独立实现,避免字段污染与权限混淆。
2.2 使用 Pydantic v2 模型严格约束各端响应体
小程序不需要updated_at时间戳,PC 端必须返回creator_avatar_url;这些不能靠前端 if 判断,而应由后端模型强制裁剪:
# app/schemas/item.py from pydantic import BaseModel from typing import Optional class ItemBase(BaseModel): name: str status: int # 小程序端只读模型(无创建人信息、无更新时间) class ItemMPResponse(ItemBase): id: int qr_code: str # 小程序专属字段:扫码核销用 class Config: orm_mode = True # PC 端管理模型(含审计字段、关联用户信息) class ItemPCResponse(ItemBase): id: int created_at: str updated_at: str creator_name: str creator_avatar_url: Optional[str] = None class Config: orm_mode = True2.2.1 在 endpoint 中显式指定响应模型,禁用response_model=Any
# app/api/mp/items.py from fastapi import Depends, HTTPException from app.schemas.item import ItemMPResponse from app.services.item import get_item_by_id_for_mp @router.get("/items/{item_id}", response_model=ItemMPResponse) def read_item_mp( item_id: int, current_user: User = Depends(get_current_mp_user) # 小程序专用鉴权依赖 ): item = get_item_by_id_for_mp(item_id, current_user.openid) if not item: raise HTTPException(status_code=404, detail="Item not found for this user") return item # 自动序列化为 ItemMPResponse,字段被严格裁剪2.3 Redis ACL 鉴权:用 redis.conf 控制不同终端的 key 访问粒度
微信小程序需缓存用户 session(mp:session:{openid}),PC 管理员需缓存菜单权限树(pc:menu:role:{role_id}),二者绝不能混用同一 Redis DB 或同一 ACL 规则。必须在redis.conf中启用 ACL 并定义角色:
# redis.conf 片段(需重启 Redis 生效) aclfile /etc/redis/users.acl # users.acl 文件内容 user mp on >mp_password ~mp:* -@all +get +set +expire user pc on >pc_password ~pc:* ~sys:* -@all +get +set +hgetall +hmset +expire对应 FastAPI 中的连接初始化:
# app/core/redis_client.py import redis from redis.connection import ConnectionPool from app.core.config import settings # 小程序专用连接池(仅能访问 mp:* key) mp_redis_pool = ConnectionPool( host=settings.REDIS_HOST, port=settings.REDIS_PORT, username="mp", password=settings.REDIS_MP_PASSWORD, db=0, decode_responses=True ) # PC 端专用连接池(可访问 pc:* 和 sys:*) pc_redis_pool = ConnectionPool( host=settings.REDIS_HOST, port=settings.REDIS_PORT, username="pc", password=settings.REDIS_PC_PASSWORD, db=0, decode_responses=True )注意:
username必须与users.acl中定义的用户名一致;~mp:*表示只允许操作以mp:开头的 key,-@all +get表示禁止所有命令,仅开放GET和SET—— 这是防止小程序端误删 PC 权限缓存的关键防线。
3. Vue3 PC 端构建:环境变量隔离与动态菜单加载
3.1 用.env文件区分开发/测试/生产 API 基地址,禁止硬编码
Vue3 项目根目录下必须存在三套环境文件:
.env.development # VUE_APP_API_BASE_URL=https://dev-api.example.com .env.staging # VUE_APP_API_BASE_URL=https://staging-api.example.com .env.production # VUE_APP_API_BASE_URL=https://api.example.com在vite.config.ts中注入:
// vite.config.ts import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig(({ mode }) => ({ plugins: [vue()], define: { __API_BASE__: JSON.stringify(process.env.VUE_APP_API_BASE_URL) } }))组件中使用:
<script setup lang="ts"> import { ref, onMounted } from 'vue' import { useApi } from '@/composables/useApi' const menuList = ref<any[]>([]) const api = useApi() // 内部自动读取 __API_BASE__ onMounted(async () => { const res = await api.get('/pc/menus') // 实际请求 https://api.example.com/pc/menus menuList.value = res.data }) </script>3.2 动态菜单不从后端全量拉取,而是按角色预置 JSON Schema
后端/api/v1/pc/menus返回的不是原始菜单数组,而是符合 JSON Schema 的结构描述,前端根据 schema 渲染并校验:
// 后端返回示例(/api/v1/pc/menus) { "schema": { "type": "array", "items": { "type": "object", "properties": { "id": {"type": "string"}, "title": {"type": "string"}, "icon": {"type": "string"}, "path": {"type": "string"}, "permission": {"type": "string"} // 如 "menu:user:list" } } }, "data": [ { "id": "user", "title": "用户管理", "icon": "UserOutlined", "path": "/users", "permission": "menu:user:list" } ] }Vue3 组件内做运行时校验:
// composables/useMenu.ts import { ref, onMounted } from 'vue' import Ajv from 'ajv' import { useApi } from './useApi' const ajv = new Ajv() const menuSchema = { type: "array", items: { type: "object", properties: { id: { type: "string" }, title: { type: "string" }, path: { type: "string" }, permission: { type: "string" } }, required: ["id", "title", "path"] } } export function useMenu() { const menuList = ref([]) const validate = ajv.compile(menuSchema) const load = async () => { const res = await useApi().get('/pc/menus') if (!validate(res.data)) { console.error('Menu data validation failed:', validate.errors) throw new Error('Invalid menu structure') } menuList.value = res.data } return { menuList, load } }3.2.1 路由守卫中集成权限校验,拒绝无权限跳转
// router/index.ts import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router' import { useAuthStore } from '@/stores/auth' const routes: Array<RouteRecordRaw> = [ { path: '/users', name: 'UserList', component: () => import('@/views/users/List.vue'), meta: { permission: 'menu:user:list' } } ] const router = createRouter({ history: createWebHistory(), routes }) router.beforeEach((to, from, next) => { const authStore = useAuthStore() if (to.meta.permission && !authStore.hasPermission(to.meta.permission)) { next({ name: 'NoAccess' }) } else { next() } }) export default router4. 微信小程序端对接:code 换 token 流程与本地缓存穿透
4.1 小程序登录必须走wx.login()→code→ 后端换openid全链路
前端调用:
// pages/login/login.js Page({ data: { loading: false }, async onGetUserInfo(e) { this.setData({ loading: true }) try { const loginRes = await wx.login() // 获取 code const userInfoRes = await wx.getUserProfile({ lang: 'zh_CN' }) // 发送 code 和加密数据给后端 const res = await wx.request({ url: 'https://api.example.com/api/v1/mp/auth/login', method: 'POST', data: { code: loginRes.code, encryptedData: userInfoRes.encryptedData, iv: userInfoRes.iv } }) if (res.data.token) { wx.setStorageSync('mp_token', res.data.token) wx.switchTab({ url: '/pages/index/index' }) } } catch (err) { console.error(err) } finally { this.setData({ loading: false }) } } })4.1.1 FastAPI 后端接收 code 并调用微信接口换取 openid
# app/api/mp/auth.py import httpx from fastapi import APIRouter, Body, HTTPException from app.schemas.auth import MPLoginRequest, MPLoginResponse from app.core.config import settings router = APIRouter() @router.post("/auth/login", response_model=MPLoginResponse) async def mp_login(payload: MPLoginRequest = Body(...)): # 1. 调用微信接口换取 openid wx_url = f"https://api.weixin.qq.com/sns/jscode2session" params = { "appid": settings.WX_MP_APPID, "secret": settings.WX_MP_SECRET, "js_code": payload.code, "grant_type": "authorization_code" } async with httpx.AsyncClient() as client: wx_res = await client.get(wx_url, params=params) wx_data = wx_res.json() if "errcode" in wx_data: raise HTTPException(status_code=400, detail=f"WeChat error: {wx_data.get('errmsg')}") # 2. 解密用户数据(此处省略 AES-128-CBC 解密逻辑,实际需用 crypto ) # 3. 创建或获取用户,生成 JWT token token = create_mp_jwt_token(openid=wx_data["openid"]) return {"token": token, "expires_in": 3600}4.2 小程序本地缓存必须穿透 Redis,避免重复拉取公共配置
小程序启动时需加载系统配置(如客服电话、服务协议 URL),但不应每次启动都请求后端。采用「本地 Storage + Redis 双检」策略:
// utils/cache.js const CONFIG_KEY = 'sys:config' async function getSysConfig() { // Step 1: 检查本地缓存 const local = wx.getStorageSync(CONFIG_KEY) if (local && local.expiredAt > Date.now()) { return local.data } // Step 2: 请求后端(带 ETag 缓存头) const res = await wx.request({ url: 'https://api.example.com/api/v1/mp/config', method: 'GET', header: { 'If-None-Match': local?.etag || '' } }) if (res.statusCode === 304 && local) { // 服务端未变更,延长本地过期时间 wx.setStorageSync(CONFIG_KEY, { ...local, expiredAt: Date.now() + 1000 * 60 * 30 // 延长30分钟 }) return local.data } if (res.statusCode === 200) { const data = res.data wx.setStorageSync(CONFIG_KEY, { data, etag: res.header['ETag'], expiredAt: Date.now() + 1000 * 60 * 15 // 15分钟过期 }) return data } throw new Error('Failed to fetch config') }后端 FastAPI 支持 ETag:
# app/api/mp/config.py from fastapi import APIRouter, Response from app.core.redis_client import mp_redis_pool import json router = APIRouter() @router.get("/config") async def get_config(response: Response): cache_key = "mp:config:latest" r = mp_redis_pool.get_connection() cached = await r.get(cache_key) if cached: etag = f'"{hash(cached)}"' response.headers["ETag"] = etag return json.loads(cached) # 生成新配置(从 DB 或静态文件读取) config = {"service_phone": "400-123-4567", "terms_url": "/terms.html"} await r.setex(cache_key, 900, json.dumps(config)) # 15分钟 response.headers["ETag"] = f'"{hash(json.dumps(config))}"' return config5. Docker 部署实战:Dockerfile 多阶段构建与 .env 安全挂载
5.1 Dockerfile 必须分离构建与运行阶段,镜像体积压缩至 128MB 以内
# Dockerfile FROM python:3.11-slim AS builder WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir --upgrade pip && \ pip install --no-cache-dir -r requirements.txt FROM python:3.11-slim # 创建非 root 用户 RUN addgroup -g 1001 -f appgroup && \ adduser -S appuser -u 1001 # 复制依赖与代码 WORKDIR /app COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages COPY --chown=appuser:appgroup . . # 暴露端口 EXPOSE 8000 # 切换用户 USER appuser # 启动命令(使用 uvicorn,非默认的 python -m uvicorn) CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0:8000", "--port", "8000", "--workers", "4", "--reload"]提示:
--chown=appuser:appgroup确保文件属主为非 root 用户;--workers 4适配 2 核 CPU,避免 GIL 争抢;--reload仅用于开发,生产环境应移除。
5.2 使用 docker-compose.yml 统一管理 FastAPI + Redis + Nginx
# docker-compose.yml version: '3.8' services: api: build: . image: admin-backend:1.2.0 restart: unless-stopped environment: - REDIS_HOST=redis - REDIS_PORT=6379 - REDIS_MP_PASSWORD=mp_secret_123 - REDIS_PC_PASSWORD=pc_secret_456 - WX_MP_APPID=${WX_MP_APPID} - WX_MP_SECRET=${WX_MP_SECRET} depends_on: - redis networks: - backend redis: image: redis:7-alpine command: redis-server /usr/local/etc/redis.conf volumes: - ./redis.conf:/usr/local/etc/redis.conf:ro - redis_data:/data networks: - backend nginx: image: nginx:alpine ports: - "80:80" - "443:443" volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro - ./ssl:/etc/nginx/ssl:ro - ./dist:/usr/share/nginx/html:ro # Vue3 构建产物 depends_on: - api networks: - backend volumes: redis_data: networks: backend: driver: bridge5.2.1 redis.conf 关键安全配置项说明
# redis.conf 片段(启用 ACL、禁用危险命令、绑定内网) bind 127.0.0.1 172.20.0.2 # 仅绑定容器内网 IP protected-mode yes port 6379 tcp-backlog 511 timeout 0 tcp-keepalive 300 daemonize no supervised auto pidfile /var/run/redis_6379.pid loglevel notice logfile "" databases 16 always-show-logo yes set-proc-title yes proc-title-template "{title} {listen-addr} {server-mode}" stop-writes-on-bgsave-error yes rdbcompression yes rdbchecksum yes dbfilename dump.rdb rdb-del-sync-files no dir /data replica-serve-stale-data yes replica-read-only yes repl-diskless-sync no repl-diskless-sync-delay 5 repl-diskless-load disabled repl-disable-tcp-nodelay no replica-priority 100 aclfile /etc/redis/users.acl # 启用 ACL 文件 # 禁用危险命令 rename-command FLUSHDB "" rename-command FLUSHALL "" rename-command KEYS "" rename-command CONFIG "" rename-command DEBUG ""5.3 生产环境必须通过 .env 文件注入敏感配置,禁止 ENV 曝光
项目根目录下.env文件(不提交 Git):
# .env WX_MP_APPID=wx1234567890abcdef WX_MP_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx REDIS_HOST=redis REDIS_PORT=6379 REDIS_MP_PASSWORD=mp_secret_123 REDIS_PC_PASSWORD=pc_secret_456 JWT_SECRET_KEY=your_32_byte_secret_key_here_1234567890abdocker-compose.yml中引用:
services: api: # ... env_file: - .env注意:
.env文件中的变量会自动注入容器环境,但docker-compose config命令会显示明文值,因此该文件必须加入.gitignore,且 CI/CD 流水线中应使用 secret manager 注入,而非直接挂载文件。
6. 接口联调验证:用 curl 模拟三端真实请求链路
6.1 验证 PC 端管理员登录与菜单拉取(带 Bearer Token)
# 1. 模拟管理员登录(返回 JWT) curl -X POST http://localhost:8000/api/v1/pc/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"123456"}' \ -w "\nHTTP Status: %{http_code}\n" # 2. 拿到 token 后请求菜单(必须带 Authorization) TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.xxxxx" curl -X GET http://localhost:8000/api/v1/pc/menus \ -H "Authorization: Bearer $TOKEN" \ -w "\nHTTP Status: %{http_code}\n" | jq '.data[0].title'预期输出:
"用户管理" HTTP Status: 2006.2 验证小程序端 code 换 token 流程(模拟微信服务器返回)
# 模拟微信返回的 openid(实际需真机调试获取 code) curl -X POST http://localhost:8000/api/v1/mp/auth/login \ -H "Content-Type: application/json" \ -d '{ "code": "mock_code_123", "encryptedData": "mock_encrypted", "iv": "mock_iv" }' \ -w "\nHTTP Status: %{http_code}\n"提示:本地开发时可在
app/api/mp/auth.py中临时添加 mock 分支,当code == "mock_code_123"时直接返回固定openid="mock_openid_abc",避免每次都要真机扫码。
6.3 验证 Redis ACL 是否生效:用 redis-cli 切换用户测试权限
# 进入 redis 容器 docker exec -it your_project_redis_1 redis-cli -a pc_secret_456 # 切换为 pc 用户(应成功) 127.0.0.1:6379> AUTH pc pc_secret_456 OK # 尝试读取 pc:* key(应成功) 127.0.0.1:6379> GET pc:menu:role:1 "..." # 尝试读取 mp:* key(应失败:NOAUTH Authentication required) 127.0.0.1:6379> GET mp:session:abc (error) NOAUTH Authentication required # 退出,切换为 mp 用户 127.0.0.1:6379> AUTH mp mp_secret_123 OK # 尝试读取 mp:* key(应成功) 127.0.0.1:6379> GET mp:session:abc "..." # 尝试读取 pc:* key(应失败:NOPERM) 127.0.0.1:6379> GET pc:menu:role:1 (error) NOPERM this user has no permissions to run the 'get' command此验证确认 ACL 规则已精确生效:PC 用户无法触达小程序数据,小程序用户无法越权访问管理配置,从存储层就切断了横向越权路径。
本文还有配套的精品资源,点击获取