:SpringBoot+Vue實(shí)戰(zhàn))
1. 項(xiàng)目概述作為一名有10年Java全棧開(kāi)發(fā)經(jīng)驗(yàn)的工程師最近我完成了一個(gè)基于Java的短劇推薦系統(tǒng)畢業(yè)設(shè)計(jì)項(xiàng)目。這個(gè)系統(tǒng)采用了當(dāng)前主流的SpringBootVue前后端分離架構(gòu)整合了推薦算法、用戶(hù)行為分析等核心功能模塊。在實(shí)際開(kāi)發(fā)過(guò)程中我遇到了不少技術(shù)難點(diǎn)和業(yè)務(wù)邏輯的挑戰(zhàn)今天就來(lái)詳細(xì)分享一下這個(gè)項(xiàng)目的設(shè)計(jì)思路和實(shí)現(xiàn)過(guò)程。短劇推薦系統(tǒng)本質(zhì)上是一個(gè)內(nèi)容分發(fā)平臺(tái)核心目標(biāo)是通過(guò)分析用戶(hù)的觀看歷史和偏好為其推薦可能感興趣的短劇內(nèi)容。相比傳統(tǒng)的影視推薦系統(tǒng)短劇具有時(shí)長(zhǎng)更短、更新頻率更快的特點(diǎn)這對(duì)推薦算法的實(shí)時(shí)性和準(zhǔn)確性提出了更高要求。2. 系統(tǒng)架構(gòu)設(shè)計(jì)2.1 技術(shù)棧選型在技術(shù)選型上我主要考慮了以下幾個(gè)因素開(kāi)發(fā)效率作為畢業(yè)設(shè)計(jì)項(xiàng)目需要在有限時(shí)間內(nèi)完成學(xué)習(xí)成本使用主流技術(shù)棧便于后續(xù)維護(hù)和擴(kuò)展性能要求需要支撐一定的并發(fā)訪問(wèn)量基于這些考慮最終確定的技術(shù)方案如下后端技術(shù)?;A(chǔ)框架Spring Boot 2.7.xORM框架MyBatis-Plus 3.5.x安全框架Spring Security緩存Redis消息隊(duì)列RabbitMQ用于異步處理用戶(hù)行為日志搜索引擎Elasticsearch用于短劇內(nèi)容檢索前端技術(shù)棧核心框架Vue 3.xUI組件庫(kù)Element Plus狀態(tài)管理Pinia路由Vue RouterHTTP客戶(hù)端Axios數(shù)據(jù)庫(kù)主庫(kù)MySQL 8.0事務(wù)型數(shù)據(jù)從庫(kù)MySQL 8.0讀寫(xiě)分離文檔數(shù)據(jù)庫(kù)MongoDB存儲(chǔ)用戶(hù)行為日志2.2 系統(tǒng)架構(gòu)圖整個(gè)系統(tǒng)采用標(biāo)準(zhǔn)的微服務(wù)架構(gòu)分為以下幾個(gè)核心服務(wù)[用戶(hù)端] → [API Gateway] → [用戶(hù)服務(wù)] → [MySQL] ↘ [推薦服務(wù)] → [Redis/ES] ↘ [內(nèi)容服務(wù)] → [MySQL] ↘ [日志服務(wù)] → [MongoDB]API Gateway使用Spring Cloud Gateway實(shí)現(xiàn)負(fù)責(zé)路由轉(zhuǎn)發(fā)、限流和鑒權(quán)。各服務(wù)之間通過(guò)OpenFeign進(jìn)行通信使用Nacos作為服務(wù)注冊(cè)中心。2.3 數(shù)據(jù)庫(kù)設(shè)計(jì)數(shù)據(jù)庫(kù)設(shè)計(jì)遵循第三范式主要包含以下幾張核心表用戶(hù)表(user)CREATE TABLE user ( id bigint NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 用戶(hù)名, password varchar(100) NOT NULL COMMENT 密碼, phone varchar(20) DEFAULT NULL COMMENT 手機(jī)號(hào), avatar varchar(255) DEFAULT NULL COMMENT 頭像, gender tinyint DEFAULT 0 COMMENT 性別(0-未知 1-男 2-女), birthday date DEFAULT NULL COMMENT 生日, status tinyint DEFAULT 1 COMMENT 狀態(tài)(0-禁用 1-正常), create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY idx_username (username), KEY idx_phone (phone) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用戶(hù)表;短劇表(drama)CREATE TABLE drama ( id bigint NOT NULL AUTO_INCREMENT, title varchar(100) NOT NULL COMMENT 短劇標(biāo)題, cover varchar(255) NOT NULL COMMENT 封面圖, description text COMMENT 描述, category_id int NOT NULL COMMENT 分類(lèi)ID, tags varchar(255) DEFAULT NULL COMMENT 標(biāo)簽逗號(hào)分隔, episode_count int NOT NULL DEFAULT 0 COMMENT 集數(shù), duration int DEFAULT NULL COMMENT 單集時(shí)長(zhǎng)(秒), score decimal(3,1) DEFAULT 0.0 COMMENT 評(píng)分, status tinyint NOT NULL DEFAULT 1 COMMENT 狀態(tài)(0-下架 1-上架), create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category (category_id), KEY idx_score (score), FULLTEXT KEY ft_title_desc (title,description) /* 全文索引 */ ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT短劇表;用戶(hù)行為表(user_behavior)CREATE TABLE user_behavior ( id bigint NOT NULL AUTO_INCREMENT, user_id bigint NOT NULL, drama_id bigint NOT NULL, behavior_type tinyint NOT NULL COMMENT 1-瀏覽 2-播放 3-點(diǎn)贊 4-收藏 5-分享, duration int DEFAULT NULL COMMENT 觀看時(shí)長(zhǎng)(秒), create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_drama (user_id,drama_id), KEY idx_create_time (create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用戶(hù)行為表;3. 核心功能實(shí)現(xiàn)3.1 推薦算法實(shí)現(xiàn)推薦系統(tǒng)是短劇平臺(tái)的核心競(jìng)爭(zhēng)力我實(shí)現(xiàn)了以下幾種推薦策略基于內(nèi)容的推薦public ListDrama recommendByContent(Long userId, int size) { // 1. 獲取用戶(hù)最近觀看的短劇 ListLong recentDramas behaviorMapper.selectRecentWatchedDramas(userId, 5); if (recentDramas.isEmpty()) { return recommendHotDramas(size); // 退回?zé)衢T(mén)推薦 } // 2. 提取這些短劇的特征標(biāo)簽 SetString tags dramaMapper.selectTagsByDramaIds(recentDramas); // 3. 根據(jù)標(biāo)簽相似度推薦其他短劇 return dramaMapper.selectByTags(tags, size); }協(xié)同過(guò)濾推薦public ListDrama recommendByCF(Long userId, int size) { // 1. 獲取相似用戶(hù) ListLong similarUsers userSimilarityService.findSimilarUsers(userId, 5); // 2. 獲取這些用戶(hù)喜歡但當(dāng)前用戶(hù)未看過(guò)的短劇 ListLong candidateDramas behaviorMapper.selectDramasByUsers( similarUsers, Collections.singletonList(userId), size * 3 ); // 3. 按熱度排序 return dramaMapper.selectByIdsOrderByScore(candidateDramas, size); }實(shí)時(shí)推薦RabbitListener(queues behavior.queue) public void processBehavior(BehaviorMessage message) { // 1. 記錄用戶(hù)行為 behaviorMapper.insert(message.toBehavior()); // 2. 更新用戶(hù)興趣模型 userProfileService.updateUserInterest(message.getUserId(), message.getDramaId(), message.getBehaviorType()); // 3. 更新短劇熱度 dramaStatService.incrementCount(message.getDramaId(), message.getBehaviorType()); }3.2 用戶(hù)行為分析用戶(hù)行為數(shù)據(jù)是推薦系統(tǒng)的重要輸入我設(shè)計(jì)了完善的行為采集和分析流程行為類(lèi)型定義public enum BehaviorType { VIEW(1, 瀏覽), // 瀏覽詳情頁(yè) PLAY(2, 播放), // 開(kāi)始播放 LIKE(3, 點(diǎn)贊), // 點(diǎn)贊 COLLECT(4, 收藏), // 收藏 SHARE(5, 分享); // 分享 private final int code; private final String desc; // constructor getters }行為權(quán)重配置# 行為權(quán)重配置 behavior.weight.view1 behavior.weight.play3 behavior.weight.like5 behavior.weight.collect8 behavior.weight.share10興趣度計(jì)算public void updateUserInterest(Long userId, Long dramaId, BehaviorType behaviorType) { // 1. 獲取行為權(quán)重 int weight getBehaviorWeight(behaviorType); // 2. 獲取短劇標(biāo)簽 SetString tags dramaService.getDramaTags(dramaId); // 3. 更新用戶(hù)興趣向量 userInterestService.incrementTagsWeight(userId, tags, weight); // 4. 更新Redis緩存 String cacheKey user:interest: userId; redisTemplate.opsForValue().set(cacheKey, userInterestService.getUserInterest(userId), 1, TimeUnit.HOURS); }3.3 性能優(yōu)化為了提升系統(tǒng)性能我實(shí)施了以下幾項(xiàng)優(yōu)化措施多級(jí)緩存設(shè)計(jì)public Drama getDramaById(Long id) { // 1. 查詢(xún)本地緩存 Drama drama localCache.get(id); if (drama ! null) { return drama; } // 2. 查詢(xún)Redis String redisKey drama: id; drama (Drama) redisTemplate.opsForValue().get(redisKey); if (drama ! null) { localCache.put(id, drama); // 回填本地緩存 return drama; } // 3. 查詢(xún)數(shù)據(jù)庫(kù) drama dramaMapper.selectById(id); if (drama ! null) { redisTemplate.opsForValue().set(redisKey, drama, 30, TimeUnit.MINUTES); localCache.put(id, drama); } return drama; }數(shù)據(jù)庫(kù)讀寫(xiě)分離spring: datasource: master: url: jdbc:mysql://master-host:3306/drama username: root password: 123456 slave: url: jdbc:mysql://slave-host:3306/drama username: root password: 123456Elasticsearch搜索優(yōu)化public SearchResponse searchDramas(String keyword, int page, int size) { NativeSearchQueryBuilder queryBuilder new NativeSearchQueryBuilder() .withQuery(QueryBuilders.multiMatchQuery(keyword, title, description, tags)) .withPageable(PageRequest.of(page - 1, size)) .withSort(SortBuilders.scoreSort()) .withHighlightFields( new HighlightBuilder.Field(title).preTags(em).postTags(/em), new HighlightBuilder.Field(description).preTags(em).postTags(/em) ); // 添加分類(lèi)和評(píng)分過(guò)濾 if (categoryId ! null) { queryBuilder.withFilter(QueryBuilders.termQuery(category_id, categoryId)); } if (minScore ! null) { queryBuilder.withFilter(QueryBuilders.rangeQuery(score).gte(minScore)); } return elasticsearchTemplate.search(queryBuilder.build(), Drama.class); }4. 系統(tǒng)部署與測(cè)試4.1 部署架構(gòu)系統(tǒng)采用Docker容器化部署整體架構(gòu)如下[Nginx] → [Spring Boot Apps] → [MySQL Cluster] ↘ [Vue App] ↘ [Redis Cluster] ↘ [Elasticsearch Cluster]使用Docker Compose編排服務(wù)version: 3 services: nginx: image: nginx:1.21 ports: - 80:80 volumes: - ./nginx.conf:/etc/nginx/nginx.conf depends_on: - gateway - frontend gateway: build: ./gateway ports: - 8080:8080 environment: - SPRING_PROFILES_ACTIVEprod depends_on: - redis - nacos frontend: build: ./frontend ports: - 3000:3000 redis: image: redis:6.2 ports: - 6379:6379 volumes: - redis_data:/data mysql: image: mysql:8.0 ports: - 3306:3306 environment: - MYSQL_ROOT_PASSWORD123456 - MYSQL_DATABASEdrama volumes: - mysql_data:/var/lib/mysql # 其他服務(wù)配置...4.2 壓力測(cè)試使用JMeter進(jìn)行壓力測(cè)試主要測(cè)試以下幾個(gè)場(chǎng)景用戶(hù)登錄接口線程數(shù)100循環(huán)次數(shù)10平均響應(yīng)時(shí)間128ms錯(cuò)誤率0%短劇推薦接口線程數(shù)200循環(huán)次數(shù)20平均響應(yīng)時(shí)間235ms錯(cuò)誤率0.2%短劇搜索接口線程數(shù)150循環(huán)次數(shù)15平均響應(yīng)時(shí)間189ms錯(cuò)誤率0%測(cè)試結(jié)果表明系統(tǒng)在常規(guī)負(fù)載下表現(xiàn)良好但在推薦接口高并發(fā)時(shí)出現(xiàn)了少量錯(cuò)誤后續(xù)通過(guò)增加Redis節(jié)點(diǎn)和優(yōu)化SQL查詢(xún)解決了這個(gè)問(wèn)題。4.3 安全測(cè)試系統(tǒng)進(jìn)行了全面的安全測(cè)試主要包括SQL注入測(cè)試使用SQLMap工具掃描所有接口均未發(fā)現(xiàn)注入漏洞XSS測(cè)試嘗試在前端輸入框中注入腳本后端過(guò)濾和前端轉(zhuǎn)義雙重防護(hù)CSRF測(cè)試所有修改操作都要求攜帶Token關(guān)鍵操作需要二次驗(yàn)證權(quán)限測(cè)試普通用戶(hù)無(wú)法訪問(wèn)管理員接口垂直權(quán)限和水平權(quán)限檢查完善5. 項(xiàng)目總結(jié)與經(jīng)驗(yàn)分享在開(kāi)發(fā)這個(gè)短劇推薦系統(tǒng)的過(guò)程中我積累了一些寶貴的經(jīng)驗(yàn)關(guān)于推薦算法不要一開(kāi)始就追求復(fù)雜的算法簡(jiǎn)單的基于內(nèi)容的推薦也能取得不錯(cuò)的效果用戶(hù)冷啟動(dòng)問(wèn)題可以通過(guò)熱門(mén)推薦和隨機(jī)推薦來(lái)解決實(shí)時(shí)性對(duì)短劇推薦特別重要用戶(hù)的最新行為應(yīng)該盡快影響推薦結(jié)果關(guān)于性能優(yōu)化緩存是提升性能的利器但要處理好緩存一致性問(wèn)題數(shù)據(jù)庫(kù)查詢(xún)一定要用EXPLAIN分析執(zhí)行計(jì)劃適當(dāng)?shù)姆捶妒皆O(shè)計(jì)可以顯著減少聯(lián)表查詢(xún)關(guān)于工程實(shí)踐接口設(shè)計(jì)要遵循RESTful規(guī)范代碼分層要清晰Controller只做參數(shù)校驗(yàn)和結(jié)果封裝日志記錄要完整便于問(wèn)題排查遇到的坑與解決方案問(wèn)題MySQL連接數(shù)不足 解決調(diào)整連接池配置增加從庫(kù)分擔(dān)讀壓力問(wèn)題Redis緩存穿透 解決對(duì)空結(jié)果也進(jìn)行緩存設(shè)置較短的過(guò)期時(shí)間問(wèn)題Elasticsearch分片不均 解決手動(dòng)調(diào)整分片分配策略設(shè)置合適的分片數(shù)這個(gè)項(xiàng)目從技術(shù)選型到最終部署上線歷時(shí)約3個(gè)月時(shí)間。通過(guò)這個(gè)項(xiàng)目我不僅鞏固了Java全棧開(kāi)發(fā)的技能還對(duì)推薦系統(tǒng)、高并發(fā)處理有了更深的理解。希望我的經(jīng)驗(yàn)分享對(duì)正在做類(lèi)似項(xiàng)目的同學(xué)有所幫助。