概述
今天要解决两件事:把用户控制面板剩下的功能(通用设置、陌生人问题、通知设置、黑名单)补齐,同时在项目里引入第二个存储引擎 MongoDB,用于「今日佳人」这类关系型数据库不好存的推荐数据。
纲要
- 回顾:完善用户信息的关键产出
- 本次主线:按讲义顺序拆解本章结构
- 通用设置:需求与表结构、查询设置、陌生人问题、通知设置、黑名单管理(分页查询 / 移除)
- MongoDB 简介:特点、Docker 安装、体系结构、数据类型
- MongoDB 入门:库表操作、增删改查、索引、执行计划
- SpringData-Mongo:环境搭建与基本操作
- 今日佳人:recommend_user 表结构、mf-dubbo-mongo 服务提供者、接口实现
- 推荐好友列表
- 依赖关系:组件套路与鉴权体系是今天的地基,MongoDB 是新引擎
- 产出预告:本章结束时工程的模块变化
回顾
之前围绕「完善用户信息」展开,产出直接服务今天的开发:
| 产出 | 一句话回顾 |
|---|---|
| OSS 对象存储 | 头像上传走阿里云 OSS,不落本地磁盘 |
| 人脸识别组件 | 对接阿里云人脸检测,校验头像是否含人脸 |
| 资料 CRUD | UserInfo 的查询与更新,走 mfController → mfService → Dubbo 的链路 |
| 拦截器 + ThreadLocal | Token 校验统一收口在拦截器,用户 Id 放 ThreadLocal 透传,业务层随取随用 |
| 统一异常处理 | @ControllerAdvice 兜底,异常转标准响应结构 |
这套「Controller 瘦身、Service 编排、Dubbo 调后端、ThreadLocal 传身份」的套路,今天写的每一个接口都会照搬。写代码前先把这个模板在脑子里过一遍,能省不少时间。
现在任务书
本章讲义结构如下,任务按这个顺序推进:
| 板块 | 做什么 | 产出物 | 验收标准 |
|---|---|---|---|
| 通用设置 | 需求分析、设计 Settings/Question/BlackList 三张表与实体类,实现查询通用设置接口 | SettingsController + SettingsService + SettingsApi 全链路 | GET 请求能返回当前用户的通知开关与陌生人问题 |
| 陌生人问题 | 保存 / 更新用户的陌生人问题 | QuestionApi 及实现 | 更新后再次查询返回新问题 |
| 通知设置 | 保存通知开关状态 | SettingsApi 的通知更新逻辑 | 数据库中 like_comment 等字段随请求变化 |
| 黑名单管理 | 分页查询黑名单、移除黑名单 | BlackListApi + BlackListMapper + 分页插件配置 | 分页返回用户信息列表,移除后列表减少一条 |
| MongoDB 简介 | 理解文档型数据库定位,Docker 安装,掌握体系结构与数据类型 | 可运行的 mongodb 容器 | docker ps 能看到容器,mongo 客户端可连接 |
| MongoDB 入门 | 库与集合操作、insert/save/update/remove、find 查询、索引创建、explain 执行计划 | 一组 mongosh 命令实操记录 | 能说出 COLLSCAN 与 IXSCAN 的区别 |
| SpringData-Mongo | 在 mf-dubbo-mongo 模块中集成 Spring Data MongoDB | MongoTemplate 的 CRUD 代码 | 单元测试完成增删改查 |
| 今日佳人 | 设计 recommend_user 表,实现 RecommendUserApi,查询缘分值最高的用户 | mf-dubbo-mongo 服务提供者 + mfService 调用 | 首页返回今日佳人卡片数据 |
| 推荐好友列表 | 基于推荐数据实现好友推荐列表接口 | RecommendUserApi 扩展 | 列表分页返回推荐用户 |
前四个板块都是「用户设置面板」的一部分,走 MySQL + MyBatis-Plus 的老路子;后面五个板块转向 MongoDB,是本章真正的重头戏。
通用设置:本章的第一块硬骨头
不要被「通用设置」这个名字骗了,它不是一个接口,而是四个接口的组合:查询设置、设置陌生人问题、设置通知、黑名单管理(查询 + 移除)。表结构先行:
#mermaid-svg-LuUMI7xFWcwcOztk{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-LuUMI7xFWcwcOztk .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-LuUMI7xFWcwcOztk .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-LuUMI7xFWcwcOztk .error-icon{fill:#552222;}#mermaid-svg-LuUMI7xFWcwcOztk .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-LuUMI7xFWcwcOztk .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-LuUMI7xFWcwcOztk .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-LuUMI7xFWcwcOztk .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-LuUMI7xFWcwcOztk .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-LuUMI7xFWcwcOztk .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-LuUMI7xFWcwcOztk .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-LuUMI7xFWcwcOztk .marker{fill:#333333;stroke:#333333;}#mermaid-svg-LuUMI7xFWcwcOztk .marker.cross{stroke:#333333;}#mermaid-svg-LuUMI7xFWcwcOztk svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-LuUMI7xFWcwcOztk p{margin:0;}#mermaid-svg-LuUMI7xFWcwcOztk .entityBox{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-LuUMI7xFWcwcOztk .relationshipLabelBox{fill:hsl(80, 100%, 96.2745098039%);opacity:0.7;background-color:hsl(80, 100%, 96.2745098039%);}#mermaid-svg-LuUMI7xFWcwcOztk .relationshipLabelBox rect{opacity:0.5;}#mermaid-svg-LuUMI7xFWcwcOztk .labelBkg{background-color:rgba(248.6666666666, 255, 235.9999999999, 0.5);}#mermaid-svg-LuUMI7xFWcwcOztk .edgeLabel .label{fill:#9370DB;font-size:14px;}#mermaid-svg-LuUMI7xFWcwcOztk .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-LuUMI7xFWcwcOztk .edge-pattern-dashed{stroke-dasharray:8,8;}#mermaid-svg-LuUMI7xFWcwcOztk .node rect,#mermaid-svg-LuUMI7xFWcwcOztk .node circle,#mermaid-svg-LuUMI7xFWcwcOztk .node ellipse,#mermaid-svg-LuUMI7xFWcwcOztk .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-LuUMI7xFWcwcOztk .relationshipLine{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-LuUMI7xFWcwcOztk .marker{fill:none!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-LuUMI7xFWcwcOztk .edgeLabel{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-LuUMI7xFWcwcOztk .edgeLabel .label rect{fill:rgba(232,232,232, 0.8);}#mermaid-svg-LuUMI7xFWcwcOztk .edgeLabel .label text{fill:#333;}#mermaid-svg-LuUMI7xFWcwcOztk :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 一对多(user_id 关联)
黑名单记录
settings
Long
id
PK
Long
user_id
Integer
like_notification
Integer
comment_notification
Integer
pinglun_notification
question
Long
id
PK
Long
user_id
String
txt
black_list
Long
id
PK
Long
user_id
Long
black_user_id
黑名单分页查询有个细节:黑名单表只存 user_id,页面要展示的是对方的头像和昵称,所以实现里要拿 BlackListApi 查出 id 列表后再调 UserInfoApi 补全用户信息,还要给 mf-dubbo-db 配上 MyBatis-Plus 的分页插件。这是本节最容易漏的一步。
MongoDB:为什么要引入第二存储引擎
「今日佳人」要存的是推荐关系:用户 A 与候选 B 的缘分值,每天刷新,量级是百万级用户 × 候选集合。这种数据用 MySQL 存,要么建一张巨宽的关联表,要么频繁 join,都不划算。MongoDB 的文档模型恰好适合:
| 维度 | MySQL | MongoDB |
|---|---|---|
| 数据模型 | 关系表,结构固定 | BSON 文档,字段可扩展 |
| 查询语言 | SQL | MQL(find / update 等方法) |
| 事务 | 强事务 | 4.0 起支持副本集内多文档事务 |
| 适用场景 | 设置、用户资料等强结构数据 | 推荐结果、访客记录、动态等海量半结构数据 |
本章只做入门三件事:Docker 起容器、mongosh 把增删改查和索引过一遍、用 explain 看执行计划。体系结构上记住一条对应关系就够:MySQL 的 database / table / row,在 MongoDB 里是 database / collection / document。
SpringData-Mongo 与今日佳人
Spring Data 家族的风格是统一的模板 + Repository 抽象。工程里新建 mf-dubbo-mongo 模块作为服务提供者,用 MongoTemplate 操作文档:
java
// 查询缘分值最高的推荐用户(节选自 RecommendUserApiImpl 思路)
Query query = Query.query(Criteria.where("userId").is(userId))
.with(Sort.by(Sort.Direction.DESC, "score"))
.skip(0).limit(1);
RecommendUser ru = mongoTemplate.findOne(query, RecommendUser.class);
对应的工程结构变化,本章结束时的快照:
tree
mf
├── mf-app-server # 应用服务:SettingsController、mfController
├── mf-autoconfig # 自动配置:OSS、人脸组件
├── mf-commons # 公共工具
├── mf-model # 实体:domain(Settings/Question/BlackList)、mongo(RecommendUser)
└── mf-dubbo
├── mf-dubbo-interface # API 接口:SettingsApi、QuestionApi、BlackListApi、RecommendUserApi
├── mf-dubbo-db # MySQL 服务提供者:SettingsApiImpl、BlackListApiImpl 等
└── mf-dubbo-mongo # MongoDB 服务提供者:DubboMongoApplication、RecommendUserApiImpl
注意 mf-model 里出现了两个包:domain 放 MySQL 实体,mongo 放 MongoDB 文档实体,两套注解体系(@TableName 对 @Document)不要混用。
依赖关系
#mermaid-svg-q0y6GxEPpq61cjsE{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-q0y6GxEPpq61cjsE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-q0y6GxEPpq61cjsE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-q0y6GxEPpq61cjsE .error-icon{fill:#552222;}#mermaid-svg-q0y6GxEPpq61cjsE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-q0y6GxEPpq61cjsE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-q0y6GxEPpq61cjsE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-q0y6GxEPpq61cjsE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-q0y6GxEPpq61cjsE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-q0y6GxEPpq61cjsE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-q0y6GxEPpq61cjsE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-q0y6GxEPpq61cjsE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-q0y6GxEPpq61cjsE .marker.cross{stroke:#333333;}#mermaid-svg-q0y6GxEPpq61cjsE svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-q0y6GxEPpq61cjsE p{margin:0;}#mermaid-svg-q0y6GxEPpq61cjsE .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-q0y6GxEPpq61cjsE .cluster-label text{fill:#333;}#mermaid-svg-q0y6GxEPpq61cjsE .cluster-label span{color:#333;}#mermaid-svg-q0y6GxEPpq61cjsE .cluster-label span p{background-color:transparent;}#mermaid-svg-q0y6GxEPpq61cjsE .label text,#mermaid-svg-q0y6GxEPpq61cjsE span{fill:#333;color:#333;}#mermaid-svg-q0y6GxEPpq61cjsE .node rect,#mermaid-svg-q0y6GxEPpq61cjsE .node circle,#mermaid-svg-q0y6GxEPpq61cjsE .node ellipse,#mermaid-svg-q0y6GxEPpq61cjsE .node polygon,#mermaid-svg-q0y6GxEPpq61cjsE .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-q0y6GxEPpq61cjsE .rough-node .label text,#mermaid-svg-q0y6GxEPpq61cjsE .node .label text,#mermaid-svg-q0y6GxEPpq61cjsE .image-shape .label,#mermaid-svg-q0y6GxEPpq61cjsE .icon-shape .label{text-anchor:middle;}#mermaid-svg-q0y6GxEPpq61cjsE .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-q0y6GxEPpq61cjsE .rough-node .label,#mermaid-svg-q0y6GxEPpq61cjsE .node .label,#mermaid-svg-q0y6GxEPpq61cjsE .image-shape .label,#mermaid-svg-q0y6GxEPpq61cjsE .icon-shape .label{text-align:center;}#mermaid-svg-q0y6GxEPpq61cjsE .node.clickable{cursor:pointer;}#mermaid-svg-q0y6GxEPpq61cjsE .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-q0y6GxEPpq61cjsE .arrowheadPath{fill:#333333;}#mermaid-svg-q0y6GxEPpq61cjsE .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-q0y6GxEPpq61cjsE .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-q0y6GxEPpq61cjsE .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-q0y6GxEPpq61cjsE .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-q0y6GxEPpq61cjsE .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-q0y6GxEPpq61cjsE .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-q0y6GxEPpq61cjsE .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-q0y6GxEPpq61cjsE .cluster text{fill:#333;}#mermaid-svg-q0y6GxEPpq61cjsE .cluster span{color:#333;}#mermaid-svg-q0y6GxEPpq61cjsE div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-q0y6GxEPpq61cjsE .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-q0y6GxEPpq61cjsE rect.text{fill:none;stroke-width:0;}#mermaid-svg-q0y6GxEPpq61cjsE .icon-shape,#mermaid-svg-q0y6GxEPpq61cjsE .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-q0y6GxEPpq61cjsE .icon-shape p,#mermaid-svg-q0y6GxEPpq61cjsE .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-q0y6GxEPpq61cjsE .icon-shape .label rect,#mermaid-svg-q0y6GxEPpq61cjsE .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-q0y6GxEPpq61cjsE .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-q0y6GxEPpq61cjsE .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-q0y6GxEPpq61cjsE :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 完善用户信息
拦截器+ThreadLocal / 统一异常 / OSS
通用设置四大接口
今日佳人
MySQL + MyBatis-Plus
含分页插件
MongoDB Docker 部署
SpringData-Mongo 集成
黑名单分页查询
跨 MySQL 用户信息补全
day04+ 圈子 / 推荐
继续消费 MongoDB 数据
两条线在这里交汇:设置面板继续吃 MySQL,推荐数据开始吃 MongoDB,之后的圈子、访客、小视频会持续使用 MongoDB,所以本章的环境搭建和 MongoTemplate 用法必须练熟。
误解澄清
| 常见误解 | 实际情况 |
|---|---|
| MongoDB 可以完全替换 MySQL | 不替换。两者并存,各管一类数据,项目里通过两个 dubbo 模块隔离 |
| save 和 insert 效果一样 | insert 遇到相同 _id 会报错;save 存在则更新、不存在则插入,语义不同 |
| 黑名单分页只查一张表 | 只查 black_list 得到 id,还要调 UserInfoApi 补头像昵称,是两步操作 |
| 分页插件全局自动生效 | 需要在 mf-dubbo-db 显式配置 MybatisPlusInterceptor,否则 Page 查询不生效 |
| MongoDB 没有索引概念 | 有 createIndex,且 explain 能看到 COLLSCAN(全表扫)与 IXSCAN(索引扫)的差异 |
下一站
结构铺完了,从这里开始逐个击破:先做通用设置的需求分析与表结构设计。