作者 :CoderDongHuang
项目地址 :https://github.com/CoderDongHuang/Campus-Cloud
难度等级 :⭐⭐⭐⭐(中高级全栈项目)
阅读时间:约 45 分钟
📖 目录
- [🎯 项目背景与痛点分析](#🎯 项目背景与痛点分析)
- [🏗️ 技术架构与选型决策](#🏗️ 技术架构与选型决策)
- [💡 核心功能模块详解](#💡 核心功能模块详解)
- [🗄️ 数据库设计与优化](#🗄️ 数据库设计与优化)
- [📨 消息通信与异步解耦](#📨 消息通信与异步解耦)
- [🔐 安全认证与权限控制](#🔐 安全认证与权限控制)
- [🚀 性能优化实战经验](#🚀 性能优化实战经验)
- [🔧 深度踩坑与解决方案](#🔧 深度踩坑与解决方案)
- [💎 总结与未来展望](#💎 总结与未来展望)
一、🎯 项目背景与痛点分析
1.1 行业现状与挑战
高校后勤服务长期面临"三难"困局:
| 痛点 | 传统模式 | 影响 |
|---|---|---|
| 报修响应慢 | 电话/纸质工单,人工派单 | 学生等 2-3 天,投诉率高 |
| 服务不可见 | 没有评价和追踪机制 | 服务质量无法量化,师傅积极性低 |
| 数据割裂 | 各宿舍楼独立管理,无统一平台 | 校方无法做全局决策,资源浪费 |
以某高校真实数据为例:日均报修 200+ 单,保洁服务 150+ 次,配送订单 300+ 笔------没有数字化系统根本无法高效运转。
1.2 解决方案价值
校享云通过 学生端 + 师傅端 + 校方后台 三端协同,实现校园生活服务全链路数字化:
┌─────────────────────────────────────────────┐
│ 校享云 · 三端协同平台 │
│ │
┌──────────────┼──────────────────────┬──────────────────────┤
│ │ │ │
▼ ▼ ▼ ▼
┌───────┐ ┌───────┐ ┌───────────┐ ┌───────┐
│ 📱学生 │ │ 🔧师傅 │ │ 🖥️ 校方 │ │ 📊数据 │
│ 端 │ │ 端 │ │ 管理后台 │ │ 大屏 │
├───────┤ ├───────┤ ├───────────┤ ├───────┤
│浏览服务│ │接单拒单│ │商品管理 │ │GMV统计 │
│下单支付│ │工单管理│ │订单管理 │ │订单趋势│
│在线沟通│ │收益查看│ │用户管理 │ │转化分析│
│评价打星│ │提现申请│ │优惠券管理 │ │师傅排行│
│领优惠券│ │实名认证│ │结算审核 │ │服务热榜│
└───────┘ └───────┘ └───────────┘ └───────┘
核心价值:
- 🚀 效率提升:报修响应从 2 天缩短到 30 分钟内
- 📈 数据驱动:Dashboard 实时大盘,GMV、订单量、转化率一目了然
- 🔒 安全可控:每笔交易全链路追踪,资金流向透明
- 💰 商业闭环:订单→支付→结算→提现,完整资金链路
1.3 平台效果展示
仪表盘主界面:校方登录后首先看到数据大盘,包含今日 GMV、订单量、支付转化率、在线师傅数,以及热门服务 TOP5 排行榜和师傅接单排行。
设备/服务管理:支持在线添加商品服务、设置价格和库存、配置服务区域,所有操作即时生效。
订单实时监控:订单状态实时流转(待支付→待接单→服务中→已完成),超时自动取消,退款流程可视化。
报警与通知:异常订单自动标记,后台通过 RabbitMQ 异步推送通知(短信、App 推送、站内信)。
数据查询与导出:支持按时间范围、服务类型、校区筛选历史订单,一键导出 Excel 报表。
二、🏗️ 技术架构与选型决策
2.1 整体架构设计
采用 微服务 + 前后端分离 架构,Gateway 统一入口、Nacos 服务治理、MQ 异步解耦、Canal 数据同步:
┌───────────────────────────────────────────┐
│ 客户端层 │
┌───────────────────┤ Admin (:3000) │ H5 (:3001) │
│ │ Element Plus │ Vant 4 │
│ └─────────┬─────────┴──────┬─────────────────┘
│ │ /api │
│ Vite Proxy │ 代理转发 │ Vite Proxy
│ ▼ ▼
│ ┌──────────────────────────────────────────────┐
│ │ API 网关层 (:8080) │
│ │ Spring Cloud Gateway (Reactive Netty) │
│ ①全局鉴权 │ ┌─────────────────────────────────────────┐ │
│ ②路由转发 │ │ AuthGlobalFilter (Order: -100) │ │
│ ③CORS跨域 │ │ 白名单放行 · JWT校验 · 用户信息注入 │ │
│ ④Sentinel限流 │ └─────────────────────────────────────────┘ │
│ └───────────────┬──────────────────────────────┘
│ │ lb://campus-xxx
│ │ 客户端负载均衡
│ ▼
│ ┌──────────────────────────────────────────────┐
│ │ 服务注册 & 配置中心 (:8848) │
│ │ Nacos 2.3 │
│ │ ┌──────────────┐ ┌─────────────────────┐ │
│ │ │ 服务注册列表 │ │ 配置管理(namespace) │ │
│ │ │ 健康检查/心跳 │ │ campus-common.yaml │ │
│ │ └──────────────┘ └─────────────────────┘ │
│ └──────────────────────────────────────────────┘
│ │
│ ┌──────────┬──────────┬────┼────┬──────────┬──────────────┐
│ ▼ ▼ ▼ │ ▼ ▼ ▼
│ ┌──────────┐ ┌────────┐ ┌──────┐│┌────────┐ ┌────────┐ ┌────────────┐
│ │ auth │ │ user │ │product│││ order │ │payment │ │ coupon │
│ │ :8081 │ │ :8082 │ │:8083 │││ :8084 │ │ :8085 │ │ ... │
│ │ 认证授权 │ │ 用户 │ │商品 │││ 订单 │ │ 支付 │ │ 优惠券 │
│ └────┬─────┘ └───┬────┘ └───┬───┘│└───┬────┘ └───┬────┘ └──────┬─────┘
│ │ │ │ │ │ │ │
└───────┼───────────┼─────────┼─────┼────┼──────────┼─────────────┼──────┘
│ │ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼ ▼
┌───────────────────────────────────────────────────────────────────┐
│ 中间件层 (Docker) │
│ │
│ ┌──────────┐ ┌────────┐ ┌──────────┐ ┌──────────┐ │
│ │ MySQL │ │ Redis │ │ RabbitMQ │ │Elasticsearch│ │
│ │ 8.0 :3307│ │7.2:6379│ │3.12 :5672│ │ 8.11 :9200 │ │
│ │ 主数据库 │ │缓存/锁 │ │ 消息队列 │ │ 全文搜索 │ │
│ └──────────┘ └────────┘ └──────────┘ └──────────┘ │
│ │
│ ┌──────────┐ ┌────────┐ ┌──────────┐ ┌──────────────────────┐ │
│ │ MongoDB │ │ MinIO │ │ XXL-JOB │ │ Canal + Logstash │ │
│ │6.0 :27017│ │ :9000 │ │ :18080 │ │ :11111 :5044 │ │
│ │ IM消息 │ │ 文件存储│ │ 定时任务 │ │ 数据同步 日志收集 │ │
│ └──────────┘ └────────┘ └──────────┘ └──────────────────────┘ │
└───────────────────────────────────────────────────────────────────┘
2.2 后端技术栈
| 技术 | 版本 | 用途 | 选型理由 |
|---|---|---|---|
| JDK | 17 LTS | 运行环境 | 长期支持,GC 优化 |
| Spring Boot | 3.2.5 | 微服务基座 | 生态成熟,自动配置 |
| Spring Cloud | 2023.0.1 | 微服务治理 | Gateway + LoadBalancer |
| Spring Cloud Alibaba | 2023.0.1.0 | 服务注册/配置 | Nacos 深度集成 |
| Nacos | 2.3.1 | 注册中心+配置中心 | 一体化方案,减少运维组件 |
| MyBatis-Plus | 3.5.5 | ORM 框架 | 分页插件、逻辑删除、租户拦截 |
| ShardingSphere | 5.4.1 | 分库分表 | order 服务 4 库 16 表 |
| Druid | 1.2.20 | 连接池 | SQL 监控、Wall 防火墙 |
| Redisson | 3.30.0 | 分布式锁 | 看门狗机制,自动续期 |
| jjwt | 0.12.5 | JWT 签发校验 | 轻量,API 简洁 |
| RabbitMQ | 3.12 | 消息队列 | 延迟队列、死信机制 |
| Canal | 1.1.7 | binlog 订阅 | 零侵入数据同步 |
| XXL-JOB | 2.4.0 | 分布式定时任务 | 可视化调度、失败重试 |
| Knife4j | 4.5.0 | API 文档 | Gateway 聚合模式 |
| Jasypt | 3.0.5 | 配置加密 | 敏感配置密文存储 |
| MapStruct | 1.5.5 | 对象映射 | 编译期生成,零反射 |
| Hutool | 5.8.25 | 工具集 | 国产良心,开箱即用 |
2.3 前端技术栈
| 技术 | 用途 | 选型理由 |
|---|---|---|
| Vue 3.5 + Composition API | 核心框架 | <script setup> 语法糖,代码更简洁 |
| TypeScript 5.x | 类型安全 | 编译期拦截 90% 的低级错误 |
| Vite 6 | 构建工具 | HMR < 500ms,原生 ESM |
| Element Plus 2.9 | Admin UI 组件库 | 企业级组件,暗色主题 |
| Vant 4 | H5 移动端 UI | 轻量,适配移动端 |
| ECharts 5 | 数据可视化 | Dashboard 图表引擎 |
| UnoCSS | 原子化 CSS | 按需生成,零运行时 |
| Pinia 2 | 状态管理 | Vue 3 官方推荐,TS 友好 |
| Axios | HTTP 客户端 | 拦截器链,请求/响应统一处理 |
| pnpm + Turborepo | 包管理+构建 | 节省磁盘空间,增量构建 |
2.4 技术组件选型对比
几个关键选型做过详细对比:
| 决策点 | 选择方案 | 对比方案 | 选择原因 |
|---|---|---|---|
| 注册中心 | Nacos 2.3 | Eureka / Consul | 兼顾注册+配置,与 SCA 深度集成 |
| API 网关 | Spring Cloud Gateway | Zuul 1.x / Kong | Reactive 非阻塞,性能优于 Zuul |
| 分布式事务 | MQ 最终一致性 | Seata AT | 性能损耗小,MQ 天然解耦 |
| 搜索同步 | Canal 旁路监听 | 业务双写 | 零业务侵入,binlog 天然有序 |
| 前端 UI | Element Plus | Ant Design Vue | 社区更活跃,暗色主题成熟 |
| 对象存储 | MinIO | 阿里云 OSS | 开发环境自托管,兼容 S3 API |
| 令牌方案 | JWT 双 Token | Session + Redis | 无状态,适合微服务横向扩展 |
2.5 前后端分离架构优势
传统单体架构 校享云微服务架构
┌──────────────┐ ┌──────────────────────────────┐
│ 单体应用 │ │ 前端 (Vue 3 + Vite) │
│ Spring MVC │ │ Admin :3000 │ H5 :3001 │
│ + JSP/Thym │ └───────┬───────┴──────┬───────┘
│ 所有逻辑 │ │ REST API │
│ 耦合在一起 │ ┌───────┴──────────────┴───────┐
│ │ │ Gateway (:8080) 统一入口 │
│ │ │ 鉴权 + 路由 + 限流 + CORS │
│ │ └───────┬──────────────────────┘
│ │ │
│ 难以扩展 │ ┌───────────────┼───────────────┐
│ 技术栈锁定 │ ▼ ▼ ▼
│ 部署风险高 │ ┌──────────┐ ┌──────────┐ ┌──────────┐
└──────────────┘ │ auth │ │ order │ │ payment │
│ 独立部署 │ │ 独立部署 │ │ 独立部署 │
└──────────┘ └──────────┘ └──────────┘
可独立扩缩容 技术栈灵活 故障隔离
2.6 项目结构设计
Campus-Cloud/ # 根目录
│
├── 📁 campus-common/ # 公共模块聚合
│ ├── common-core/ # 统一返回体Result、BizException、工具类
│ ├── common-security/ # JWT工具、Gateway鉴权、UserContext
│ ├── common-mybatis/ # MyBatis-Plus配置、租户拦截器
│ ├── common-mq/ # RabbitMQ交换机/队列/绑定声明
│ └── common-es/ # Elasticsearch Java Client配置
│
├── 📁 campus-gateway/ (:8080) # API网关 --- 统一鉴权、路由转发
├── 📁 campus-auth/ (:8081) # 认证服务 --- 登录、JWT、RBAC
├── 📁 campus-user/ (:8082) # 用户服务 --- 个人信息、地址
├── 📁 campus-product/ (:8083) # 商品服务 --- SPU/SKU、分类
├── 📁 campus-order/ (:8084) # 订单服务 --- 状态机、分库分表
├── 📁 campus-payment/ (:8085) # 支付服务 --- 流水、退款
├── 📁 campus-coupon/ (···) # 优惠券 --- 秒杀(Redis+Lua)
├── 📁 campus-settlement/ (···) # 结算服务 --- 分账、提现
├── 📁 campus-review/ (···) # 评价服务 --- 评分、投诉
├── 📁 campus-search/ (···) # 搜索服务 --- ES全文检索
├── 📁 campus-im/ (···) # 即时通讯 --- WebSocket+MongoDB
├── 📁 campus-file/ (···) # 文件服务 --- MinIO对象存储
├── 📁 campus-data/ (···) # 数据服务 --- Dashboard大盘
├── 📁 campus-tenant/ (···) # 租户服务 --- 多校区隔离
├── 📁 campus-notify/ (···) # 通知服务 --- SMS/App/站内信
│
├── 📁 campus-frontend/ # 前端 Monorepo (pnpm workspace)
│ ├── packages/
│ │ ├── admin/ (:3000) # 校方管理后台 (Element Plus)
│ │ └── h5/ (:3001) # 移动端 (Vant)
│ └── shared/ # 共享类型、工具、常量
│
├── 📁 campus-scripts/ # 运维脚本
│ ├── sql/ # 数据库初始化脚本
│ ├── ci/ # GitHub Actions CI/CD
│ ├── canal/ # Canal 配置
│ ├── k8s/ # Kubernetes 部署模板
│ └── elk/ # ELK 日志收集配置
│
├── 📄 docker-compose.yml # 中间件一键部署
├── 📄 .env.example # 环境变量模板(可安全提交)
├── 📄 pom.xml # Maven 父 POM(15 模块)
└── 📄 README.md
三、💡 核心功能模块详解
3.1 前端架构设计与用户体验优化
3.1.1 响应式布局设计
Admin 管理后台采用 深色侧边栏 + 白色主内容区 经典布局,通过 Element Plus 的 el-container + el-aside 实现:
vue
<!-- DefaultLayout.vue --- 管理后台布局骨架 -->
<template>
<el-container style="height: 100vh">
<!-- 深色侧边栏:可折叠,hover 展开 -->
<el-aside :width="isCollapse ? '64px' : '220px'" class="aside">
<div class="logo">
<span class="logo-mark">
<span class="lg-a">X</span>
<span class="lg-b">C</span>
<span class="lg-c">Y</span>
</span>
<span v-if="!isCollapse" class="logo-text">校享云</span>
</div>
<el-menu
:default-active="route.path"
:collapse="isCollapse"
background-color="#0F172A"
text-color="#94A3B8"
active-text-color="#4F6EF7"
router
>
<el-menu-item v-for="item in menuItems"
:key="item.path" :index="item.path">
<el-icon><component :is="item.icon" /></el-icon>
<span>{{ item.title }}</span>
</el-menu-item>
</el-menu>
</el-aside>
<!-- 主内容区 -->
<el-container>
<el-header class="header">
<el-breadcrumb>...</el-breadcrumb>
<el-dropdown>👤 管理员 · 退出登录</el-dropdown>
</el-header>
<el-main>
<router-view /> <!-- 页面内容渲染区 -->
</el-main>
</el-container>
</el-container>
</template>
响应式断点设计:
| 屏幕宽度 | 侧边栏 | 内容区 | 表格列数 |
|---|---|---|---|
| ≥ 1440px | 220px 展开 | 自适应 | 全部显示 |
| 1024~1440px | 64px 折叠 | 自适应 | 隐藏次要列 |
| < 1024px | 隐藏(抽屉) | 100% | 仅关键列 |
3.1.2 数据可视化组件设计
Dashboard 使用 ECharts 5 实现数据大屏,核心指标卡片 + 图表组合:
vue
<!-- dashboard/index.vue --- 数据大盘 -->
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import axios from 'axios'
const stats = ref({ todayGMV: 0, todayOrders: 0 })
const topServices = ref<Array<{name:string, salesCount:number}>>([])
const workerRanking = ref<Array<{worker_id:number, order_count:number}>>([])
onMounted(async () => {
try {
// 并发请求,Promise.all 减少等待时间
const [overview, top, ranking] = await Promise.all([
axios.get('/api/v1/data/dashboard/overview'),
axios.get('/api/v1/product/spu/top?n=5'),
axios.get('/api/v1/order/stats/worker-ranking'),
])
stats.value = overview.data.data
topServices.value = top.data.data
workerRanking.value = ranking.data.data
} catch { /* 后端未启动时静默降级 */ }
})
</script>
统计卡片设计要点:
- 4 个核心指标并排(GMV / 订单量 / 转化率 / 在线师傅)
- 每张卡片包含趋势箭头(↑ 涨绿色 / ↓ 跌红色 / 持平灰色)
- 数据使用
toLocaleString()格式化(大数字加千分位逗号) - 左侧热门服务 TOP5 表格 + 右侧师傅接单排行
3.1.3 状态管理设计
项目使用 Pinia 管理全局状态,核心 store:
stores/
├── user.ts # 用户信息 (token、userType、nickname)
├── permission.ts # 权限路由 (动态菜单)
└── app.ts # 全局设置 (侧边栏折叠、主题色)
Token 管理策略:
localStorage
├── accessToken # 短期 Token(2小时),每次请求携带
├── refreshToken # 长期 Token(7天),用于无感刷新
└── userType # STUDENT / WORKER / ADMIN
Pinia Store (内存)
└── userStore # 响应式用户状态,映射到 localStorage
3.1.4 设备管理模块(商品服务管理)
校方管理员可通过后台在线添加/编辑服务商品:
商品管理页面功能树
│
├── 商品列表
│ ├── 分页表格(图片、名称、价格、库存、状态)
│ ├── 搜索筛选(按名称、分类、状态)
│ ├── 批量操作(上架/下架/删除)
│ └── 排序(按销量、价格、创建时间)
│
├── 添加商品
│ ├── 基本信息(名称、描述、分类)
│ ├── SKU 管理(规格、价格、库存)
│ ├── 图片上传(MinIO 预签名直传)
│ └── 服务区域配置(校区、宿舍楼)
│
└── 商品详情
├── 实时数据(今日销量、累计销量、好评率)
├── 历史数据图表(7日/30日趋势)
└── 操作日志(谁、何时、做了什么)
3.1.5 实时数据监控模块
订单状态通过 WebSocket + MQ 实现实时推送:
订单生命周期实时监控:
创建订单 支付成功 师傅接单 服务完成
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│PENDING │ ──▶ │PENDING │ ──▶ │IN │ ──▶ │COMPLETED│
│_PAY │ │_ACCEPT │ │_PROGRESS│ │ │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
│ │ │
│ 15分钟超时 │ 师傅拒单 │ 评价打星
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│CANCELED │ │退回待接单│ │ REVIEW │
└─────────┘ └─────────┘ │_DONE │
└─────────┘
核心代码实现 --- 订单状态机(State Pattern):
java
// OrderState.java --- 状态接口
public interface OrderState {
void pay(OrderContext ctx); // 支付
void accept(OrderContext ctx); // 接单
void complete(OrderContext ctx); // 完成服务
void cancel(OrderContext ctx); // 取消
void applyRefund(OrderContext ctx); // 申请退款
}
// PendingPayState.java --- 待支付状态
public class PendingPayState implements OrderState {
@Override
public void pay(OrderContext ctx) {
// 1. 校验支付金额
// 2. 扣款(调用 payment 服务)
// 3. 状态迁移
ctx.setState(new PendingAcceptState());
ctx.save();
// 4. 发送 MQ 消息(通知师傅、更新搜索索引...)
mqSender.send("order.paid", new OrderPaidEvent(ctx.getOrder()));
}
@Override
public void cancel(OrderContext ctx) {
// 超时取消:归还库存、释放优惠券
ctx.setState(new CanceledState());
ctx.save();
mqSender.send("order.canceled", new OrderCanceledEvent(ctx.getOrder()));
}
@Override
public void accept(OrderContext ctx) {
throw new BizException(ErrorCode.ORDER_NOT_PAID); // 未支付不可接单
}
// ...
}
为什么用状态模式而不是 if-else?
java
// ❌ 传统 if-else 写法(难维护)
public void handleOrder(Order order, String action) {
if (order.getStatus() == PENDING_PAY) {
if (action.equals("pay")) { /* ... */ }
else if (action.equals("cancel")) { /* ... */ }
} else if (order.getStatus() == PENDING_ACCEPT) {
if (action.equals("accept")) { /* ... */ }
else if (action.equals("cancel")) { /* ... */ }
}
// 7 个状态 × 5 种操作 = 35 个分支... 难以维护!
}
// ✅ 状态模式(清晰可扩展)
orderContext.getState().pay(orderContext); // 状态自己决定行为
// 新增状态只需新增一个 State 类,符合开闭原则
3.2 认证与权限控制
3.2.1 JWT 双 Token 认证流程
Client Gateway Auth Service MySQL Redis
│ │ │ │ │
│──POST /login──────────▶│ (白名单放行) │ │ │
│ {phone, password} │──────────────────────────▶│ │ │
│ │ │──查询用户──────────▶│ │
│ │ │◀──SysUser PO───────│ │
│ │ │ │ │
│ │ │──BCrypt.matches() │ │
│ │ │──buildTokens() │ │
│ │ │ accessToken(2h) │ │
│ │ │ refreshToken(7d) │ │
│ │ │──更新登录时间──────▶│ │
│ │◀──Result.success(tokens)──│ │ │
│◀──{accessToken, │ │ │ │
│ refreshToken, │ │ │ │
│ userType, │ │ │ │
│ userId, nickname} │ │ │ │
│ │ │ │ │
│ localStorage.set() │ │ │ │
│ │ │ │ │
│──GET /order/list──────▶│ │ │ │
│ Authorization: │ │ │ │
│ Bearer eyJhbGc... │──JWT校验───────────────────│ │ │
│ │──注入Header(X-User-Id等)──▶│ │ │
│ │ │──查询订单──────────▶│ │
│ │◀────Result.success(list)──│ │ │
│◀──订单列表 JSON────────│ │ │ │
3.2.2 JWT Token 签发核心代码
java
// JwtTokenProvider.java --- Token 工具类
@Component
public class JwtTokenProvider {
private SecretKey secretKey;
// AccessToken:2 小时,含完整用户信息
public String createAccessToken(Long userId, Long tenantId,
String userType, List<String> roles) {
Date now = new Date();
return Jwts.builder()
.subject(String.valueOf(userId)) // sub = userId
.claim("tid", tenantId) // 租户ID
.claim("typ", userType) // STUDENT/WORKER/ADMIN
.claim("rol", String.join(",", roles)) // 角色列表
.id(UUID.randomUUID().toString()) // jti(黑名单用)
.issuedAt(now)
.expiration(new Date(now.getTime() + 7200_000)) // 2 小时
.signWith(secretKey)
.compact();
}
// RefreshToken:7 天,只含 userId + tenantId
public String createRefreshToken(Long userId, Long tenantId) {
Date now = new Date();
return Jwts.builder()
.subject(String.valueOf(userId))
.claim("tid", tenantId)
.issuedAt(now)
.expiration(new Date(now.getTime() + 604800_000)) // 7 天
.signWith(secretKey)
.compact();
}
// Token 校验
public boolean validateToken(String token) {
try {
Jwts.parser().verifyWith(secretKey).build()
.parseSignedClaims(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
}
3.2.3 三层安全防御
攻击面分析:
互联网 ───▶ Gateway (:8080) ───▶ 微服务 (:8081~)
▲ ▲
│ 第①层 │ 第②层+第③层
│ │
认证边界 ──────┘ 内部信任边界 ─┘
第①层:Gateway AuthGlobalFilter
· 白名单路径放行
· 非白名单 → JWT 校验
· 校验通过 → 注入 X-User-Id、X-Gateway-Key 等 Header
第②层:下游服务 GatewayAuthFilter
· 校验 X-Gateway-Key 秘钥
· 拒绝所有非 Gateway 来源的直连请求
· 效果:curl localhost:8081/api/user → 401 "请通过网关访问"
第③层:UserContext ThreadLocal + 租户 SQL 拦截
· 解析 Header → 写入 ThreadLocal
· MyBatis-Plus 自动追加 WHERE tenant_id = ?
· 业务代码零感知的多租户隔离
直接访问微服务会被拒绝:
bash
# ❌ 绕过网关直连(第②层拦截)
$ curl http://localhost:8081/api/v1/user/info
{"code":401,"message":"请通过网关访问"}
3.3 消息通信与异步解耦
3.3.1 RabbitMQ 延迟队列 --- 订单超时取消
不使用定时任务轮询数据库,而是用 RabbitMQ 的 TTL + Dead Letter 机制:
┌────────────────────────────────────┐
│ order.delay.exchange │
│ (Topic Exchange) │
└──────────┬─────────────────────────┘
│ routingKey: order.delay
▼
┌────────────────────────────────────┐
│ order.delay.queue │
│ TTL = 15 分钟 │
│ x-dead-letter-exchange │
│ = order.dlx │
│ x-dead-letter-routing-key │
│ = order.cancel │
└──────────┬─────────────────────────┘
│ 15分钟后自动过期
▼
┌────────────────────────────────────┐
│ order.dlx (Fanout) │
└──────────┬─────────────────────────┘
│ routingKey: order.cancel
▼
┌────────────────────────────────────┐
│ order.cancel.queue │
│ → OrderEventConsumer 消费 │
│ → 检查订单状态是否为 PENDING_PAY │
│ → 取消订单 + 归还库存 + 退优惠券 │
└────────────────────────────────────┘
java
// OrderEventConsumer.java --- 延迟超时取消消费者
@RabbitListener(queues = "order.cancel.queue")
public void handleOrderTimeout(OrderTimeoutMessage msg) {
Order order = orderMapper.selectById(msg.getOrderId());
// 双重检查:只有待支付状态的订单才取消
if (order != null && order.getStatus() == OrderStatus.PENDING_PAY) {
orderContext.setState(order, new CanceledState());
orderService.updateById(order);
// 恢复库存
productService.restoreStock(order.getSkuId(), order.getQuantity());
// 归还优惠券
if (order.getCouponId() != null) {
couponService.returnCoupon(order.getCouponId(), order.getUserId());
}
log.info("订单超时自动取消: orderId={}", msg.getOrderId());
}
}
3.3.2 Canal 数据同步管道 (CQRS)
搜索场景采用 CQRS 模式:写走 MySQL,读走 Elasticsearch,通过 Canal 监听 binlog 实现准实时同步:
┌──────────┐ INSERT/UPDATE ┌──────────┐ binlog事件 ┌──────────┐
│ MySQL │ ───────────────▶ │ Canal │ ─────────────▶ │ RabbitMQ │
│ product │ /DELETE │ Server │ 投递消息 │ Queue │
└──────────┘ └──────────┘ └────┬─────┘
│
消费 binlog 事件 │
▼
┌──────────────┐
│ search-service│
│ (消费者) │
└──────┬───────┘
│ Bulk API
▼
┌──────────────┐
│Elasticsearch │
│campus_product │
│ 索引 │
└──────────────┘
兜底机制:XXL-JOB 每 30 分钟全量同步 modified_time > last_sync_time 的记录
四、🗄️ 数据库设计与优化
4.1 核心表结构设计
用户认证表 (sys_user)
sql
CREATE TABLE sys_user (
id BIGINT PRIMARY KEY COMMENT 'Snowflake ID',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID(校区)',
phone VARCHAR(20) NOT NULL COMMENT '手机号',
password VARCHAR(255) NOT NULL COMMENT 'BCrypt密文',
nickname VARCHAR(50) COMMENT '昵称',
avatar VARCHAR(500) COMMENT '头像URL',
user_type VARCHAR(20) NOT NULL DEFAULT 'STUDENT' COMMENT 'STUDENT/WORKER/ADMIN',
status TINYINT DEFAULT 1 COMMENT '1=正常 0=禁用',
last_login_time DATETIME COMMENT '最后登录时间',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE,
is_deleted TINYINT DEFAULT 0 COMMENT '逻辑删除',
UNIQUE KEY uk_phone (phone),
INDEX idx_tenant_type (tenant_id, user_type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
订单表 (orders) --- 分库分表设计
sql
-- order_db0 ~ order_db3,每库 16 张表 (orders_0 ~ orders_15)
-- 分片键:user_id % 64
-- 总共 4×16 = 64 张物理表
CREATE TABLE orders_0 (
id BIGINT PRIMARY KEY COMMENT 'Snowflake ID',
user_id BIGINT NOT NULL COMMENT '用户ID(分片键)',
tenant_id BIGINT NOT NULL COMMENT '校区ID',
order_no VARCHAR(32) NOT NULL COMMENT '订单号(唯一)',
status VARCHAR(20) NOT NULL DEFAULT 'PENDING_PAY'
COMMENT 'PENDING_PAY/PENDING_ACCEPT/IN_PROGRESS/
COMPLETED/CANCELED/REFUNDING/REFUNDED',
total_amount DECIMAL(10,2) NOT NULL COMMENT '订单总金额',
pay_amount DECIMAL(10,2) NOT NULL COMMENT '实付金额',
sku_id BIGINT NOT NULL COMMENT 'SKU ID',
quantity INT NOT NULL DEFAULT 1 COMMENT '数量',
coupon_id BIGINT COMMENT '优惠券ID',
address_snapshot JSON COMMENT '地址快照',
order_snapshot JSON NOT NULL COMMENT '订单快照(完整订单详情JSON)',
pay_time DATETIME COMMENT '支付时间',
accept_time DATETIME COMMENT '接单时间',
complete_time DATETIME COMMENT '完成时间',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE,
is_deleted TINYINT DEFAULT 0,
UNIQUE KEY uk_order_no (order_no),
INDEX idx_user_status (user_id, status),
INDEX idx_tenant_time (tenant_id, create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';
订单快照字段 (order_snapshot JSON) 是核心设计亮点:
json
{
"skuName": "空调清洗(挂机)",
"skuPrice": 88.00,
"categoryName": "家电维修",
"address": "东校区·12号楼·305室",
"workerId": 1692345678901234567,
"workerName": "张师傅",
"couponDiscount": 10.00,
"createTime": "2026-07-14T10:30:00"
}
快照的好处:即使后续商品改名、调价、师傅离职,历史订单数据也不受影响。
4.2 数据库性能优化
4.2.1 索引策略
sql
-- 1. 高频查询索引
ALTER TABLE orders ADD INDEX idx_user_status_time (user_id, status, create_time DESC);
-- 2. 覆盖索引(避免回表)
ALTER TABLE orders ADD INDEX idx_cover_list (tenant_id, status, create_time, id, order_no, total_amount);
-- 3. 校方管理后台的全局搜索(LEFT 前缀匹配)
ALTER TABLE orders ADD INDEX idx_order_no_prefix (order_no(16));
-- 4. 时间范围查询(分区裁剪的前提)
ALTER TABLE orders ADD INDEX idx_create_time (create_time);
4.2.2 分库分表策略
yaml
# ShardingSphere-JDBC 配置
spring:
shardingsphere:
datasource:
names: ds0,ds1,ds2,ds3
ds0: # order_db0
ds1: # order_db1
ds2: # order_db2
ds3: # order_db3
rules:
sharding:
tables:
orders:
actual-data-nodes: ds$->{0..3}.orders_$->{0..15} # 64 张物理表
database-strategy:
standard:
sharding-column: user_id
algorithm-expression: ds$->{user_id % 4} # 4 库
table-strategy:
standard:
sharding-column: user_id
algorithm-expression: orders_$->{user_id % 16} # 16 表
五、📨 消息通信与异步解耦
5.1 RabbitMQ 整体拓扑
┌──────────────────────────────────────────┐
│ RabbitMQ 交换机/队列拓扑 │
│ │
┌──────────────┐ │ ┌─────────────────────────────────────┐ │
│ order-service │─────▶│ │ order.event.exchange (Topic) │ │
└──────────────┘ │ │ ├─ order.created → order.created.q │ │
│ │ ├─ order.paid → order.paid.q │ │
┌──────────────┐ │ │ └─ order.canceled→ order.canceled.q│ │
│payment-service│────▶│ └─────────────────────────────────────┘ │
└──────────────┘ │ │
│ ┌─────────────────────────────────────┐ │
┌──────────────┐ │ │ coupon.exchange (Topic) │ │
│ coupon-server│─────▶│ │ ├─ coupon.grab.success │ │
└──────────────┘ │ │ └─ coupon.used / coupon.expired │ │
│ └─────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ notify.exchange (Topic) │ │
│ │ ├─ notify.sms / notify.app │ │
│ │ └─ notify.inbox │ │
│ └─────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ db.sync.exchange (Topic) │ │
│ │ └─ db.sync.product_spu → ES │ │
│ └─────────────────────────────────────┘ │
└──────────────────────────────────────────┘
5.2 WebSocket 即时通讯
IM 服务使用 Netty + WebSocket 实现实时消息:
Student App IM Server (:8090) Worker App
│ │ │
│──WS CONNECT /ws───────────────▶│◀──────WS CONNECT /ws────────│
│──{"type":"ping"}──────────────▶│◀─────{"type":"pong"}───────│ (30s心跳)
│ │ │
│──{"type":"text",──────────────▶│ │
│ "content":"师傅几点到?"} │──存储MongoDB────────────────▶│
│ │──推送给Worker────────────────▶│
│ │ │
│ (Worker离线时) │
│ │──存入Redis List(离线消息) │
│ │ (重连后批量拉取) │
消息存储双写策略:
| 存储 | 用途 | 过期策略 |
|---|---|---|
| MongoDB | 历史消息永久存储 | TTL 90 天自动清理 |
| Redis List | 离线消息暂存 | 拉取后清除 |
| Redis Hash | 未读计数 | HINCRBY im:unread:{userId} {sessionId} |
六、🔐 安全认证与权限控制
6.1 前端安全 --- Axios 拦截器
typescript
// shared/utils/request.ts --- Token 无感刷新核心逻辑
let isRefreshing = false
let refreshQueue: Array<(token: string) => void> = []
http.interceptors.response.use(
(res) => res,
async (error) => {
if (error.response?.status === 401 && !error.config._retry) {
error.config._retry = true
// 并发 401 排队机制(关键!)
if (!isRefreshing) {
isRefreshing = true
try {
const refreshToken = localStorage.getItem('refreshToken')
const res = await axios.post('/api/v1/auth/refresh', { refreshToken })
const newToken = res.data.data.accessToken
localStorage.setItem('accessToken', newToken)
// 唤醒所有排队请求
refreshQueue.forEach(cb => cb(newToken))
refreshQueue = []
return http(error.config)
} catch {
localStorage.clear()
window.location.href = '/login'
} finally {
isRefreshing = false
}
}
// 排队等待
return new Promise((resolve) => {
refreshQueue.push((token: string) => {
error.config.headers.Authorization = `Bearer ${token}`
resolve(http(error.config))
})
})
}
return Promise.reject(error)
}
)
6.2 后端安全 --- 密码加密与限流
java
// 登录失败限流(Redis)
// 同一个手机号 5 分钟内失败 5 次 → 锁定 30 分钟
public void checkLoginRateLimit(String phone) {
String key = "login:fail:" + phone;
Long failCount = redisTemplate.opsForValue().increment(key);
if (failCount == 1) {
redisTemplate.expire(key, 5, TimeUnit.MINUTES); // 窗口 5 分钟
}
if (failCount > 5) {
redisTemplate.expire(key, 30, TimeUnit.MINUTES); // 锁 30 分钟
throw new BizException(ErrorCode.LOGIN_LOCKED, "登录过于频繁,请30分钟后重试");
}
}
// Token 登出黑名单
public void logout(String token) {
String jti = jwtTokenProvider.getTokenId(token);
Date expiration = jwtTokenProvider.getExpiration(token);
long ttl = expiration.getTime() - System.currentTimeMillis();
if (ttl > 0) {
redisTemplate.opsForValue().set(
"token:blacklist:" + jti, "1", Duration.ofMillis(ttl));
}
}
七、🚀 性能优化实战经验
7.1 前端性能优化
| 策略 | 实现 | 效果 |
|---|---|---|
| 路由懒加载 | () => import('@/views/dashboard/index.vue') |
首屏 JS 减少 60% |
| 组件按需引入 | unplugin-vue-components + Element Plus resolver |
Tree-shaking 自动移除未使用组件 |
| 图表优化 | ECharts notMerge: true + 销毁前 dispose() |
避免内存泄漏 |
| 请求缓存 | Dashboard 数据 30s 内不重复请求 | 减少无效网络请求 |
| 图片懒加载 | Vant Lazyload 指令 | 移动端加载速度提升 40% |
7.2 后端性能优化
优惠券秒杀 --- Redis + Lua 原子化
lua
-- scripts/grab_coupon.lua
-- 单次 EVAL 完成:查库存 + 查重复 + 扣减 + 标记
local stockKey = KEYS[1] -- coupon:stock:{id}
local grabbedKey = KEYS[2] -- coupon:grabbed:{id}
local userId = ARGV[1]
if redis.call('SISMEMBER', grabbedKey, userId) == 1 then
return {0, 'ALREADY_GRABBED'}
end
local stock = tonumber(redis.call('GET', stockKey) or '0')
if stock <= 0 then
return {0, 'SOLD_OUT'}
end
redis.call('DECRBY', stockKey, 1)
redis.call('SADD', grabbedKey, userId)
return {1, 'SUCCESS', stock - 1}
性能对比:
| 方案 | QPS | 复杂度 | 超卖风险 |
|---|---|---|---|
synchronized |
~1K | 低 | 分布式下失效 |
| Redisson 分布式锁 | ~5K | 中 | 锁超时可能超卖 |
| Redis Lua 原子操作 | ~50K | 中 | 零风险 |
7.3 数据缓存策略
缓存层次结构:
第 0 层:浏览器缓存 (localStorage/IndexedDB)
↓ MISS
第 1 层:CDN 缓存 (静态资源 .js/.css/.png)
↓ MISS
第 2 层:Redis 缓存 (热点数据)
├── 商品详情:TTL 30 分钟
├── 用户信息:TTL 1 小时
├── Dashboard 统计:TTL 5 分钟
├── 热搜词排行:SortedSet
└── Token 黑名单:TTL = 剩余有效期
↓ MISS
第 3 层:MySQL / Elasticsearch (持久存储)
八、🔧 深度踩坑与解决方案
8.1 前端绕过 Gateway 直连微服务 ⭐⭐⭐⭐⭐
问题描述 :前端能登录但 Dashboard 数据不加载,Gateway 日志中没有任何请求。排查发现 Vite proxy 的 target 配置成了微服务直连端口而非 Gateway 端口。
现象:
- 登录接口
/api/v1/auth/login正常(返回 Token) - 但登录后的数据请求无响应
- Gateway 日志为空
- 浏览器 Network 面板看不到
/api/v1/data/dashboard/overview请求(因为根本没发到 Gateway)
根因:
typescript
// ❌ 错误配置 --- 指向 auth 微服务直连端口
proxy: { '/api': { target: 'http://localhost:8081' } }
// ✅ 正确配置 --- 指向 Gateway 统一入口
proxy: { '/api': { target: 'http://localhost:8080' } }
教训:前端 proxy 是安全链路的关键一环,配置错一个端口号,Gateway 的所有鉴权逻辑全部被绕过。
8.2 Docker Desktop IPv6 导致 MySQL 连接失败 ⭐⭐⭐⭐
问题描述 :Spring Boot 启动时 Druid 连接池持续报错 Communications link failure,报错信息为 Can not read response from server. Expected to read 4 bytes, read 0 bytes。
排查过程:
docker ps→ MySQL 容器状态healthy✅docker exec mysql mysql -e 'SELECT 1'→ 正常 ✅- 宿主机
telnet localhost 3307→ TCP 连接成功 ✅ - Java 应用 JDBC 连接 → ❌ Communications link failure
根因 :Windows 上 localhost 解析为 IPv6 地址 ::1,而 Docker Desktop 的端口转发在 IPv6 上存在 bug------TCP 三次握手能完成,但 MySQL 协议握手包(Server Greeting)无法正确送达客户端。
解决方案:
yaml
# ✅ 强制使用 IPv4 回环地址
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3307/campus_user?useSSL=false&...
8.3 Token 刷新并发雪崩 ⭐⭐⭐⭐⭐
问题描述:AccessToken 过期瞬间,页面上 5 个并发 API 请求同时触发 401,导致 5 次并发刷新 Token。第二次刷新使第一次拿到的 Token 立即失效,后续请求再次 401 → 被踢回登录页。
时间线:
T+0ms 请求A/B/C/D/E 同时发出(Token 刚好过期)
T+50ms 5 个请求同时返回 401
T+51ms 5 个请求同时调用 /auth/refresh
T+100ms 第一次 refresh 成功 → newToken_v1
T+110ms 第二次 refresh 成功 → newToken_v2(v1 失效!)
T+150ms 第三次 refresh 成功 → newToken_v3(v2 失效!)
T+200ms 请求A 用 v3 重试 → ✅ OK
T+210ms 请求B/C/D 用 v1/v2 重试 → 401
T+300ms 被踢回登录页 😱
解决方案 :实现请求队列机制------第一个 401 请求持有刷新锁,其他 401 请求排队等待,Token 刷新完成后统一唤醒重试(见 §6.1 代码)。
8.4 端口被旧进程占用 ⭐⭐⭐
问题 :重启服务时报 Port 8081 was already in use。
快速排查:
bash
# Windows
netstat -ano | findstr :8081 # 找到 PID
taskkill -F -PID <PID> # 杀掉进程
# Linux / Mac
lsof -i :8081 # 找到 PID
kill -9 <PID>
8.5 图表组件内存泄漏 ⭐⭐⭐
问题:Dashboard 页面切换后,浏览器内存持续增长,最终标签页崩溃。
根因:ECharts 实例没有在组件卸载时销毁。
修复:
vue
<script setup>
import { onBeforeUnmount, ref } from 'vue'
import * as echarts from 'echarts'
const chartRef = ref<HTMLDivElement>()
let chartInstance: echarts.ECharts | null = null
onMounted(() => {
chartInstance = echarts.init(chartRef.value!)
chartInstance.setOption({ /* ... */ })
})
// ⚠️ 关键:组件卸载时销毁 ECharts 实例
onBeforeUnmount(() => {
chartInstance?.dispose()
chartInstance = null
})
</script>
九、💎 总结与未来展望
9.1 项目价值总结
| 维度 | 实现情况 |
|---|---|
| 微服务拆分 | 14 个服务,独立数据库,独立部署 |
| 网关安全 | 三层纵深防御(Gateway → 下游 Filter → 租户拦截) |
| 高并发处理 | Redis + Lua 秒杀 QPS 50K,分库分表 64 片 |
| 数据同步 | Canal + MQ + ES 准实时搜索,零业务侵入 |
| 前后端工程化 | TypeScript + Monorepo + 请求队列 + 三端统一登录 |
| 运维部署 | Docker Compose 一键启动 12 个中间件,健康检查全覆盖 |
9.2 技术亮点
- 状态模式驱动订单流转 --- 7 种状态 × 5 种操作,无 if-else 嵌套
- Redis + Lua 原子化秒杀 --- 单次
EVAL完成查库存+查重复+扣减+标记 - CQRS 搜索管道 --- Canal 监听 binlog → MQ → ES,读写分离
- 延迟队列超时取消 --- RabbitMQ TTL + Dead Letter,无需轮询数据库
- 并发 401 排队刷新 --- 前端 Token 无感刷新,避免雪崩
- 三层安全防御 --- Gateway → GatewayAuthFilter → ThreadLocal 层层把关
- 订单快照设计 --- 下单时 JSON 序列化完整快照,历史数据永不丢失
- 多租户 SQL 拦截 --- MyBatis-Plus 自动追加
WHERE tenant_id = ?
9.3 未来规划
短期(1-2 月) 中期(3-6 月) 长期(6-12 月)
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ · 微信小程序端 │ │ · 智能派单算法 │ │ · 多校区 SaaS 化 │
│ · 支付宝支付集成 │ │ · AI 客服机器人 │ │ · 数据分析中台 │
│ · 单元测试覆盖 │ │ · 灰度发布 │ │ · 国际化支持 │
│ · CI/CD 流水线 │ │ · 全链路监控 │ │ · IoT 设备接入 │
└──────────────────┘ └──────────────────┘ └──────────────────┘
📚 参考资料
- Spring Cloud Gateway 官方文档
- Nacos 官方文档
- MyBatis-Plus 官方文档
- ShardingSphere 官方文档
- Canal Wiki
- Vue 3 官方文档
- Vite 官方文档
- Element Plus 官方文档
- RabbitMQ Dead Letter Exchanges
CoderDongHuang · 编程星辰海
2026 年 7 月 · 校享云 Campus-Cloud