Springboot仿抖音app開發之用戶業務模塊后端復盤及相關業務知識總結
?
?
BO類和VO類的區別
BO (Business Object) - 業務對象
- 定義: 業務對象是包含業務邏輯的領域模型
- 用途: 主要用于封裝業務邏輯相關的數據,在業務層(Service層)之間傳遞
- 特點:
- 與業務處理密切相關
- 通常包含業務處理所需的數據
- 在服務層之間或與DAO層之間傳輸數據
- 可能包含一些業務計算方法
VO (View Object) - 視圖對象
- 定義: 視圖對象是專門用于展示層的數據載體
- 用途: 主要用于封裝視圖展示所需的數據,在控制層(Controller)和視圖層(前端)之間傳遞
- 特點:
- 與界面展示密切相關
- 通常只包含頁面顯示所需的數據
- 可能是對多個實體的組合,以適應頁面展示需求
- 通常不包含業務邏輯方法
從您提供的代碼示例來看:
-
VlogBO - 業務對象:
public class VlogBO {private String id;private String vlogerId;private String url;private String cover;private String title;private Integer width;private Integer height;private Integer likeCounts;private Integer commentsCounts; }
- 包含視頻博客的核心業務數據,如ID、URL、尺寸、點贊數等
- 可能用于視頻上傳、處理等業務操作
-
VlogerVO - 視圖對象:
public class VlogerVO {private String vlogerId;private String nickname;private String face;private boolean isFollowed = true; }
- 專注于前端展示需要的數據,如視頻創作者ID、昵稱、頭像和關注狀態
- 簡化了數據結構,只包含UI層需要的信息
- 添加了UI特定的字段,如
isFollowed
,這與前端交互相關
在實際應用中的區別
-
使用場景:
- BO: 主要在service層使用,封裝業務邏輯所需的數據
- VO: 主要在controller層使用,向前端返回數據
-
轉換關系:
- 通常會有DO(Data Object) → BO → VO的轉換過程
- DO是與數據庫表結構一一對應的對象
- BO可能組合多個DO,添加業務處理
- VO進一步調整BO,使其適合前端展示
-
職責分離:
- 使用這些不同類型的對象有助于實現關注點分離
- 每一層專注于自己的職責,使代碼更易于維護
保存視頻信息入庫
1. 控制層接收請求 (VlogController)
首先,前端向 /vlog/publish
端點發送 POST 請求,攜帶視頻信息:
@PostMapping("publish")
public GraceJSONResult publish(@RequestBody VlogBO vlogBO) {vlogService.createVlog(vlogBO);return GraceJSONResult.ok();
}
這個控制器方法:
- 使用?
@RequestBody
?注解接收 JSON 格式的請求體,并自動反序列化為?VlogBO
?對象 - 調用?
vlogService.createVlog(vlogBO)
?處理業務邏輯 - 返回成功響應給前端
2. 服務層處理業務邏輯 (VlogServiceImpl)
接下來,控制器調用服務層的 createVlog
方法:
@Transactional
@Override
public void createVlog(VlogBO vlogBO) {String vid = sid.nextShort();Vlog vlog = new Vlog();BeanUtils.copyProperties(vlogBO, vlog);vlog.setId(vid);vlog.setLikeCounts(0);vlog.setCommentsCounts(0);vlog.setIsPrivate(YesOrNo.NO.type);vlog.setCreatedTime(new Date());vlog.setUpdatedTime(new Date());vlogMapper.insert(vlog);
}
在這個方法中執行了以下操作:
-
事務管理:
- 使用?
@Transactional
?注解確保整個操作在一個事務中完成
- 使用?
-
ID生成:
- 使用?
sid.nextShort()
?生成唯一的視頻ID Sid
?是一個分布式ID生成器,確保生成的ID在分布式環境中唯一
- 使用?
-
對象轉換:
- 創建數據庫實體對象?
Vlog
- 使用?
BeanUtils.copyProperties()
?將?VlogBO
?的屬性復制到?Vlog
?實體
- 創建數據庫實體對象?
-
設置默認值:
- 設置視頻ID:
vlog.setId(vid)
- 初始化點贊數和評論數為0
- 設置視頻為公開狀態:
vlog.setIsPrivate(YesOrNo.NO.type)
- 設置創建時間和更新時間為當前時間
- 設置視頻ID:
-
數據庫插入:
- 調用?
vlogMapper.insert(vlog)
?將視頻信息插入數據庫
- 調用?
3. 數據訪問層執行SQL (VlogMapper)
最后,vlogMapper
執行數據庫插入操作:
vlogMapper.insert(vlog);
這里的 VlogMapper
是一個 MyBatis 接口,會將 Vlog
對象映射為 SQL 插入語句并執行。
實現數據層mybatis自定義mapper與sql
實現查詢短視頻列表與分頁功能
1. 前端請求入口
前端通過HTTP GET請求訪問/indexList
接口,傳入以下參數:
userId
:當前用戶ID(可選)search
:搜索關鍵詞(可選)page
:頁碼pageSize
:每頁顯示條數
@GetMapping("indexList")
public GraceJSONResult indexList(@RequestParam(defaultValue = "") String userId,@RequestParam(defaultValue = "") String search,@RequestParam Integer page,@RequestParam Integer pageSize) {if (page == null) {page = COMMON_START_PAGE;}if (pageSize == null) {pageSize = COMMON_PAGE_SIZE;}PagedGridResult list = vlogService.queryIndexVlogList(userId, search, page, pageSize);return GraceJSONResult.ok(list);
}
2. 服務層處理
服務層實現分頁查詢邏輯:
@Override
public PagedGridResult queryIndexVlogList(String userId,String search,Integer page,Integer pageSize) {PageHelper.startPage(page, pageSize);Map<String, Object> map = new HashMap<>();if (StringUtils.isNotBlank(search)) {map.put("search", search);}List<IndexVlogVO> list = vlogMapperCustom.getIndexVlogList(map);// 注釋掉的代碼處理關注和點贊信息return setterPagedGrid(list, page);
}
這里有幾個關鍵點:
- 使用
PageHelper.startPage(page, pageSize)
啟動分頁功能 - 將查詢參數封裝到Map中
- 調用自定義Mapper執行查詢
- 使用
setterPagedGrid
方法封裝分頁結果
3. 數據訪問層查詢
自定義Mapper接口定義查詢方法:
public List<IndexVlogVO> getIndexVlogList(@Param("paramMap") Map<String, Object> map);
對應的XML映射文件:
xml
<select id="getIndexVlogList" resultType="com.imooc.vo.IndexVlogVO" parameterType="map">SELECTv.id as vlogId,v.vloger_id as vlogerId,u.face as vlogerFace,u.nickname as vlogerName,v.title as content,v.url as url,v.cover as cover,v.width as width,v.height as height,v.like_counts as likeCounts,v.comments_counts as commentsCounts,v.is_private as isPrivateFROMvlog vLEFT JOINusers uONv.vloger_id = u.idWHEREv.is_private = 0<if test="paramMap.search != null and paramMap.search != ''">and v.title LIKE '%${paramMap.search}%'</if>ORDER BYv.created_timeDESC</select>
這里的SQL實現了:
- 聯表查詢視頻和用戶信息
- 只查詢公開視頻(
is_private = 0
) - 條件性添加搜索條件(模糊查詢標題)
- 按創建時間降序排序
分析
-
參數注解:
@Param("paramMap")
?是MyBatis的參數注解- 它將傳入的Map參數在MyBatis中命名為"paramMap"
- 這個命名使得在XML映射文件中可以通過
paramMap
來引用這個Map
-
Map參數:
- 方法接受一個
Map<String, Object>
類型的參數 - 這個Map可以包含多個鍵值對,用于傳遞不同的查詢條件
search
是這個Map中的一個鍵,對應的值是搜索關鍵詞
- 方法接受一個
-
關聯用戶信息(通過
在視頻列表中直接顯示創作者頭像和名稱LEFT JOIN users u
)能夠在一次查詢中同時獲取視頻創作者的頭像(u.face
)和昵稱(u.nickname
),這樣可以:
? ? ? ? ?減少前端需要發起的額外請求
? ? ? ? ?提升用戶體驗,用戶可以一眼看到視頻來源
4. 分頁結果封裝
將查詢結果封裝為統一的分頁響應格式:
public PagedGridResult setterPagedGrid(List<?> list, Integer page) {PageInfo<?> pageList = new PageInfo<>(list);PagedGridResult gridResult = new PagedGridResult();gridResult.setRows(list);gridResult.setPage(page);gridResult.setRecords(pageList.getTotal());gridResult.setTotal(pageList.getPages());return gridResult;
}
這個方法利用PageInfo
類(PageHelper提供)獲取分頁元數據,并封裝到自定義的PagedGridResult
對象中。
5. PageHelper工作原理
PageHelper通過ThreadLocal變量和攔截器實現分頁功能:
- 當調用
PageHelper.startPage(page, pageSize)
時,在ThreadLocal中記錄分頁參數 - 在執行SQL前,PageHelper攔截器會修改SQL,添加分頁語句(如MySQL的LIMIT)
- 執行完SQL后,PageHelper會將查詢結果封裝到
Page
對象中 PageInfo
構造函數會自動識別Page對象并提取分頁信息
${}
和#{}
占位符的區別?
and v.title LIKE '%${paramMap.search}%'
特點:
- 直接替換:
${}
是字符串直接替換,會將參數值直接替換到SQL語句中 - 無類型處理:不會添加任何類型處理或轉義
- 無預編譯:不使用預編譯占位符,而是在SQL字符串中直接替換
例子: 如果paramMap.search = "旅行"
,生成的SQL將是:
and v.title LIKE '%旅行%'
2.?#{}
占位符 - 參數綁定
WHERE v.id = #{paramMap.vlogId}
特點:
- 參數綁定:
#{}
是參數綁定,會被替換為?
預編譯占位符 - 自動類型處理:會根據參數類型進行適當的類型處理
- 預編譯:使用預編譯語句,參數會作為PreparedStatement的參數傳遞
- 自動轉義:會自動處理特殊字符,防止SQL注入
例子: 如果paramMap.vlogId = "1001"
,實際執行時的SQL會先被處理為:
WHERE v.id = ?
然后值"1001"
作為參數綁定到這個位置。
兩者的主要區別
特性 | ${} | #{} |
---|---|---|
執行方式 | 字符串替換 | 參數綁定 |
SQL注入風險 | 高 | 低 |
類型處理 | 無 | 自動 |
預編譯支持 | 不支持 | 支持 |
性能 | 對于重復查詢較低 | 支持語句緩存,性能更好 |
使用場景 | 動態表名、列名、排序字段等 | 一般的參數傳遞 |
為什么在這兩種情況下使用不同的占位符
1. 使用${}
的場景 (模糊查詢)
and v.title LIKE '%${paramMap.search}%'
這里使用${}
是因為:
- LIKE語句中的百分號
%
需要與搜索詞直接拼接 - 使用
#{}
會將整個值(包括%
)當作一個參數,無法實現通配符功能
注意:這種用法存在SQL注入風險,更安全的方式是:
and v.title LIKE CONCAT('%', #{paramMap.search}, '%')
2. 使用#{}
的場景 (精確匹配)
WHERE v.id = #{paramMap.vlogId}
這里使用#{}
是因為:
- 是精確匹配查詢,不需要字符串拼接
- 需要防止SQL注入,提高安全性
- 可以利用預編譯提升性能
視頻詳情頁展示的實現?
1. 控制層處理
@GetMapping("detail")
public GraceJSONResult detail(@RequestParam(defaultValue = "") String userId,@RequestParam String vlogId) {return GraceJSONResult.ok(vlogService.getVlogDetailById(userId, vlogId));
}
控制層接收兩個關鍵參數:
vlogId
:要查詢的視頻ID(必傳)userId
:當前訪問用戶的ID(可選,默認為空)
這里直接調用服務層方法,并使用GraceJSONResult.ok()
包裝結果,提供統一的響應格式。
2. 服務層處理
@Override
public IndexVlogVO getVlogDetailById(String userId, String vlogId) {Map<String, Object> map = new HashMap<>();map.put("vlogId", vlogId);List<IndexVlogVO> list = vlogMapperCustom.getVlogDetailById(map);if (list != null && list.size() > 0 && !list.isEmpty()) {IndexVlogVO vlogVO = list.get(0);// return setterVO(vlogVO, userId);return vlogVO;}// 這里應該有返回null的邏輯
}
服務層主要邏輯:
- 創建參數Map,將視頻ID存入
- 調用自定義Mapper執行數據庫查詢
- 從查詢結果列表中獲取第一個元素作為結果
- 注釋掉的代碼
setterVO(vlogVO, userId)
可能原本用于設置用戶相關狀態,如是否已點贊、是否已關注視頻創作者等
3. 數據訪問層查詢
Mapper接口定義:
public List<IndexVlogVO> getVlogDetailById(@Param("paramMap") Map<String, Object> map);
XML映射文件中的SQL查詢:
<select id="getVlogDetailById" parameterType="map" resultType="com.imooc.vo.IndexVlogVO">SELECTv.id as vlogId,v.vloger_id as vlogerId,u.face as vlogerFace,u.nickname as vlogerName,v.title as content,v.url as url,v.cover as cover,v.width as width,v.height as height,v.like_counts as likeCounts,v.comments_counts as commentsCounts,v.is_private as isPrivateFROMvlog vLEFT JOINusers uONv.vloger_id = u.idWHEREv.id = #{paramMap.vlogId}</select>
這個SQL查詢:
- 聯合查詢視頻表(
vlog
)和用戶表(users
) - 通過LEFT JOIN關聯視頻創作者信息
- 使用
vlogId
作為查詢條件 - 查詢結果包含視頻基本信息和創作者信息
- 結果字段別名與VO對象屬性名對應,實現自動映射
4. 數據結構設計
從SQL查詢中可以看出,IndexVlogVO
對象包含以下信息:
-
視頻信息:
- vlogId:視頻ID
- content:視頻標題/內容
- url:視頻地址
- cover:視頻封面
- width/height:視頻尺寸
- likeCounts:點贊數
- commentsCounts:評論數
- isPrivate:是否私有
-
創作者信息:
- vlogerId:創作者ID
- vlogerFace:創作者頭像
- vlogerName:創作者昵稱
5. 業務處理特點
- 單一職責:各層次職責明確,控制層處理請求,服務層處理業務,數據訪問層執行查詢
- 數據封裝:使用VO對象封裝前端展示所需數據
- 關聯查詢:一次查詢獲取視頻和創作者信息,減少數據庫交互
- 參數傳遞:使用Map傳遞查詢參數,靈活性高
- 狀態設計:預留了設置用戶與視頻交互狀態的邏輯(被注釋掉的
setterVO
方法)
6. 實現特點與優勢
- 代碼復用:與首頁視頻列表使用類似的數據結構和查詢邏輯
- 查詢效率:通過ID直接查詢,高效精準
- 結構清晰:各層次分工明確,易于維護
- 擴展性好:預留了添加用戶交互狀態的擴展點
- 統一響應:使用
GraceJSONResult
統一API響應格式
實現轉為私密或公開視頻
1. 控制層處理請求
應用提供了兩個端點,分別用于將視頻設為私密或公開:
@PostMapping("changeToPrivate")
public GraceJSONResult changeToPrivate(@RequestParam String vlogerId,@RequestParam String Id) {vlogService.changeToPrivateOrPublic(vlogerId,Id,YesOrNo.YES.type);return GraceJSONResult.ok();
}@PostMapping("changeToPublic")
public GraceJSONResult changeToPublic(@RequestParam String vlogerId,@RequestParam String Id) {vlogService.changeToPrivateOrPublic(vlogerId,Id,YesOrNo.NO.type);return GraceJSONResult.ok();
}
這兩個方法的特點:
- 都使用HTTP POST請求
- 接收相同的參數:
vlogerId
(創作者ID)和Id
(視頻ID) - 調用同一個服務方法,但傳入不同的狀態值
- 使用枚舉
YesOrNo
來表示狀態(YES表示私密,NO表示公開) - 返回統一的成功響應
2. 服務層實現業務邏輯
服務層實現了統一的方法來處理狀態變更:
@Transactional
@Override
public void changeToPrivateOrPublic(String userId, String vlogId, Integer yesOrNo) {Example example = new Example(Vlog.class);Example.Criteria criteria = example.createCriteria();criteria.andEqualTo("id", vlogId);criteria.andEqualTo("vlogerId", userId);Vlog pendingVlog = new Vlog();pendingVlog.setIsPrivate(yesOrNo);vlogMapper.updateByExampleSelective(pendingVlog, example);
}
展示我的公開和私密視頻?
1. 控制層處理請求
應用提供了兩個端點,分別用于獲取用戶的公開和私密視頻列表:
@GetMapping("myPublicList")
public GraceJSONResult myPublicList(@RequestParam String userId,@RequestParam Integer page,@RequestParam Integer pageSize) {if (page == null) {page = COMMON_START_PAGE;}if (pageSize == null) {pageSize = COMMON_PAGE_SIZE;}PagedGridResult gridResult = vlogService.queryMyVlogList(userId,page,pageSize,YesOrNo.NO.type);return GraceJSONResult.ok(gridResult);
}@GetMapping("myPrivateList")
public GraceJSONResult myPrivateList(@RequestParam String vlogerId,@RequestParam Integer page,@RequestParam Integer pageSize) {if (page == null) {page = COMMON_START_PAGE;}if (pageSize == null) {pageSize = COMMON_PAGE_SIZE;}PagedGridResult gridResult = vlogService.queryMyVlogList(vlogerId,page,pageSize,YesOrNo.YES.type);return GraceJSONResult.ok(gridResult);
}
這兩個方法的特點:
- 都使用HTTP GET請求
- 接收相同的參數:用戶ID、頁碼和每頁大小
- 對頁碼和每頁大小提供默認值
- 調用同一個服務方法,但傳入不同的狀態值
- 返回統一格式的分頁結果
2. 服務層實現業務邏輯
服務層實現了統一的方法來查詢不同狀態的視頻:
@Override
public PagedGridResult queryMyVlogList(String userId, Integer page, Integer pageSize, Integer yesOrNo) {Example example = new Example(Vlog.class);Example.Criteria criteria = example.createCriteria();criteria.andEqualTo("vlogerId", userId);criteria.andEqualTo("isPrivate", yesOrNo);PageHelper.startPage(page, pageSize);List<Vlog> list = vlogMapper.selectByExample(example);return setterPagedGrid(list, page);
}
這個方法的關鍵點:
- 查詢條件:通過用戶ID和私密狀態篩選視頻
- 分頁實現:使用PageHelper進行分頁
- 通用Mapper:使用MyBatis通用Mapper執行查詢
- 結果封裝:調用
setterPagedGrid
方法封裝分頁結果
3. 分頁結果封裝
前面代碼片段中有setterPagedGrid
方法用于封裝分頁結果:
public PagedGridResult setterPagedGrid(List<?> list, Integer page) {PageInfo<?> pageList = new PageInfo<>(list);PagedGridResult gridResult = new PagedGridResult();gridResult.setRows(list);gridResult.setPage(page);gridResult.setRecords(pageList.getTotal());gridResult.setTotal(pageList.getPages());return gridResult;
}
該方法將原始列表和分頁信息封裝到PagedGridResult
對象中,包括:
- 數據行(rows)
- 當前頁碼(page)
- 總記錄數(records)
- 總頁數(total)
4. 枚舉使用
代碼使用了YesOrNo
枚舉來表示視頻狀態:
YesOrNo.NO.type
:表示公開視頻(值可能為0)YesOrNo.YES.type
:表示私密視頻(值可能為1)
?