在实际开发中,我们经常需要构建一个能够持续、稳定地向用户推送每日精选内容的系统。这类系统通常被称为“日推”或“每日推荐”,其核心挑战在于如何将后端的内容生成逻辑与前端用户界面无缝集成,同时保证推送的时效性、准确性和用户体验。一个典型的“日推”系统需要处理内容源管理、推荐算法、定时任务、数据存储和前端展示等多个环节。
本文将围绕一个名为“Umbrella”的日推系统概念,从零开始构建一个最小可运行的原型。我们将使用 Spring Boot 作为后端框架,Vue.js 作为前端框架,通过一个完整的 CRUD 流程来演示如何实现日推内容的增删改查和定时发布。通过这个原型,你将理解日推系统的基本架构、核心组件间的数据流转,以及在实际部署中可能遇到的典型问题及其解决方案。无论你是想学习全栈开发,还是希望为你的应用添加一个“每日推荐”模块,这篇文章都将提供一个清晰的实践路径。
1. 理解“日推”系统的核心组件与数据流
在动手编码之前,我们需要明确“日推”系统要解决的核心问题:如何自动化地、每日一次地将筛选后的内容推送给用户。这决定了系统必须包含几个关键部分。
1.1 系统核心模块划分
一个基础的日推系统通常包含以下模块:
- 内容管理模块:负责日推内容的创建、编辑、删除和查询。这是系统的数据源头。
- 定时任务模块:负责在每天指定的时间点(例如,凌晨0点)触发“发布”动作,将预设的某条内容标记为“今日推荐”。
- 内容获取接口:为前端提供一个简单的 API,用于获取“今日推荐”的内容。
- 前端展示模块:以友好的界面展示“今日推荐”,可能包括标题、图片、摘要和详情链接。
它们之间的数据流可以概括为:管理员通过后台创建多条候选内容 -> 定时任务每日选取一条并更新状态 -> 用户访问前端页面 -> 前端调用接口获取当前推荐内容 -> 页面渲染展示。
1.2 技术栈选型与原型设计
为了快速实现和演示,我们选择以下技术栈:
- 后端:Spring Boot (2.7.x 或 3.x)。它简化了配置,内嵌 Tomcat,能快速构建 RESTful API。
- 数据库:H2 Database (内存模式)。便于本地开发和测试,无需安装外部数据库。
- ORM框架:Spring Data JPA。简化数据库操作。
- 定时任务:Spring
@Scheduled注解。实现简单的每日定时触发。 - 前端:Vue.js 3 + Element Plus。构建交互式单页面应用。
- 构建工具:后端用 Maven,前端用 Vite 或 Vue CLI。
原型的数据结构非常简单,核心是一张daily_recommendation表:
| 字段名 | 类型 | 说明 |
|---|---|---|
id | Long | 主键,自增 |
title | String | 推荐内容的标题 |
content | String | 推荐内容的正文或描述 |
image_url | String | 封面图片链接 |
recommend_date | Date | 推荐日期。系统根据此字段判断哪条是“今日推荐” |
created_at | Timestamp | 创建时间 |
is_published | Boolean | 是否已发布(用于后台管理) |
关键逻辑在于:recommend_date字段存储了这条内容被计划推荐的日期。定时任务的工作就是找到recommend_date为“明天”且未发布的内容,将其is_published状态更新为true。而前端接口则始终查询recommend_date为“今天”且已发布的内容。
2. 搭建后端 Spring Boot 项目
我们首先从后端开始,构建提供数据管理和 API 的服务。
2.1 项目初始化与依赖配置
使用 Spring Initializr 或 IDE 创建项目,选择以下依赖:
- Spring Web:构建 Web 应用和 REST API。
- Spring Data JPA:数据持久化。
- H2 Database:内嵌数据库。
- Lombok:简化 POJO 代码(可选但推荐)。
生成的pom.xml关键依赖部分如下:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>2.2 数据模型与仓库层实现
首先定义实体类DailyRecommendation,对应数据库表。
package com.umbrella.dailyrecommendation.entity; import lombok.Data; import javax.persistence.*; import java.time.LocalDate; import java.time.LocalDateTime; @Entity @Table(name = "daily_recommendation") @Data // Lombok 注解,自动生成 getter, setter, toString 等 public class DailyRecommendation { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false) private String title; @Column(columnDefinition = "TEXT") private String content; private String imageUrl; @Column(nullable = false, unique = true) // 推荐日期唯一,保证每天只有一条 private LocalDate recommendDate; private Boolean isPublished = false; // 默认未发布 @Column(updatable = false) private LocalDateTime createdAt; @PrePersist protected void onCreate() { createdAt = LocalDateTime.now(); // 自动设置创建时间 } }注意:
@Column(unique = true)确保了recommend_date的唯一性,这是业务逻辑的重要约束,防止同一天被安排多条推荐。
接着,创建数据访问接口,继承JpaRepository,Spring Data JPA 会自动实现基本CRUD方法。我们还需要自定义一个根据日期查询的方法。
package com.umbrella.dailyrecommendation.repository; import com.umbrella.dailyrecommendation.entity.DailyRecommendation; import org.springframework.data.jpa.repository.JpaRepository; import java.time.LocalDate; import java.util.Optional; public interface DailyRecommendationRepository extends JpaRepository<DailyRecommendation, Long> { // 根据推荐日期查找 Optional<DailyRecommendation> findByRecommendDate(LocalDate recommendDate); // 查找已发布的今日推荐 Optional<DailyRecommendation> findByRecommendDateAndIsPublished(LocalDate recommendDate, Boolean isPublished); }2.3 业务逻辑与定时任务
创建服务类DailyRecommendationService,封装核心业务逻辑。
package com.umbrella.dailyrecommendation.service; import com.umbrella.dailyrecommendation.entity.DailyRecommendation; import com.umbrella.dailyrecommendation.repository.DailyRecommendationRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; import java.util.Optional; @Service @RequiredArgsConstructor // Lombok 为 final 字段生成构造函数 @Slf4j public class DailyRecommendationService { private final DailyRecommendationRepository repository; /** * 获取今日推荐 */ public Optional<DailyRecommendation> getTodayRecommendation() { LocalDate today = LocalDate.now(); // 查找今日日期且已发布的内容 return repository.findByRecommendDateAndIsPublished(today, true); } /** * 定时发布任务:每天凌晨1点执行 * 找到 recommendDate 为明天且未发布的内容,将其发布 */ @Scheduled(cron = "0 0 1 * * ?") // Cron 表达式:秒 分 时 日 月 周 @Transactional public void publishScheduledRecommendation() { LocalDate tomorrow = LocalDate.now().plusDays(1); Optional<DailyRecommendation> candidate = repository.findByRecommendDate(tomorrow); if (candidate.isPresent()) { DailyRecommendation rec = candidate.get(); if (!rec.getIsPublished()) { rec.setIsPublished(true); repository.save(rec); log.info("已成功发布明日({})的推荐:{}", tomorrow, rec.getTitle()); } } else { log.warn("未找到计划在明日({})推荐的内容,请提前配置。", tomorrow); } } // 其他业务方法:save, findAll, deleteById 等... }要使定时任务生效,需要在主应用类或配置类上添加@EnableScheduling注解。
package com.umbrella.dailyrecommendation; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication @EnableScheduling // 启用定时任务 public class DailyRecommendationApplication { public static void main(String[] args) { SpringApplication.run(DailyRecommendationApplication.class, args); } }2.4 构建 RESTful API 控制器
创建控制器DailyRecommendationController,对外提供 API。
package com.umbrella.dailyrecommendation.controller; import com.umbrella.dailyrecommendation.entity.DailyRecommendation; import com.umbrella.dailyrecommendation.service.DailyRecommendationService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; import java.util.Optional; @RestController @RequestMapping("/api/recommendations") @RequiredArgsConstructor @CrossOrigin(origins = "http://localhost:8080") // 允许前端跨域请求,端口根据前端实际运行端口调整 public class DailyRecommendationController { private final DailyRecommendationService service; /** * 获取今日推荐 (给前端调用) * GET /api/recommendations/today */ @GetMapping("/today") public ResponseEntity<DailyRecommendation> getToday() { Optional<DailyRecommendation> todayRec = service.getTodayRecommendation(); // 如果找不到,返回 404,或者一个默认的占位内容 return todayRec.map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } /** * 获取所有推荐内容 (后台管理用) * GET /api/recommendations */ @GetMapping public List<DailyRecommendation> getAll() { // 这里应注入后台管理服务,简单起见直接调用 repository return service.findAll(); // 需要在 Service 中实现 findAll 方法 } /** * 创建或更新推荐内容 * POST /api/recommendations */ @PostMapping public DailyRecommendation createOrUpdate(@RequestBody DailyRecommendation recommendation) { return service.save(recommendation); } /** * 删除推荐内容 * DELETE /api/recommendations/{id} */ @DeleteMapping("/{id}") public ResponseEntity<Void> delete(@PathVariable Long id) { service.deleteById(id); return ResponseEntity.ok().build(); } }2.5 配置文件与数据库初始化
在src/main/resources/application.yml中配置应用和 H2 数据库。
spring: datasource: url: jdbc:h2:mem:daily_recommend_db # 内存数据库 driver-class-name: org.h2.Driver username: sa password: jpa: database-platform: org.hibernate.dialect.H2Dialect hibernate: ddl-auto: update # 启动时更新表结构 show-sql: true # 开发时显示SQL,生产环境关闭 h2: console: enabled: true # 启用 H2 控制台,方便查看数据 path: /h2-console server: port: 8081 # 后端服务端口 # 自定义日志级别,方便观察定时任务 logging: level: com.umbrella.dailyrecommendation.service: DEBUG启动 Spring Boot 应用后,可以访问http://localhost:8081/h2-console查看数据库。JDBC URL 填写jdbc:h2:mem:daily_recommend_db。
3. 构建前端 Vue.js 管理界面
后端 API 就绪后,我们构建一个简单的前端管理界面,用于管理推荐内容和查看今日推荐。
3.1 初始化 Vue 项目并安装依赖
使用 Vite 快速创建 Vue 项目:
npm create vue@latest umbrella-daily-recommendation-frontend # 按照提示选择项目特性,确保加入 Router 和 Pinia(状态管理) cd umbrella-daily-recommendation-frontend npm install npm install element-plus axios # 安装 Element Plus 图标库(可选) npm install @element-plus/icons-vue在main.js或main.ts中引入 Element Plus 和样式。
import { createApp } from 'vue' import App from './App.vue' import router from './router' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' import * as ElementPlusIconsVue from '@element-plus/icons-vue' const app = createApp(App) for (const [key, component] of Object.entries(ElementPlusIconsVue)) { app.component(key, component) } app.use(router) app.use(ElementPlus) app.mount('#app')3.2 创建 API 请求模块
在src目录下创建utils/request.js,使用 axios 封装 HTTP 请求。
import axios from 'axios' const request = axios.create({ baseURL: 'http://localhost:8081/api', // 后端 API 地址 timeout: 5000 }) // 请求拦截器(可添加 token 等) request.interceptors.request.use( config => { // 可在此处统一添加请求头,如 Authorization return config }, error => { return Promise.reject(error) } ) // 响应拦截器 request.interceptors.response.use( response => { return response.data }, error => { console.error('API Request Error:', error) // 统一错误处理,例如提示用户 return Promise.reject(error) } ) export default request创建src/api/recommendation.js,定义与后端交互的具体接口。
import request from '@/utils/request' export function getTodayRecommendation() { return request({ url: '/recommendations/today', method: 'get' }) } export function getAllRecommendations() { return request({ url: '/recommendations', method: 'get' }) } export function createRecommendation(data) { return request({ url: '/recommendations', method: 'post', data }) } export function updateRecommendation(id, data) { // 这里假设更新也是 POST,实际可根据后端设计用 PUT return request({ url: `/recommendations/${id}`, method: 'post', data }) } export function deleteRecommendation(id) { return request({ url: `/recommendations/${id}`, method: 'delete' }) }3.3 实现管理页面组件
创建src/views/RecommendationManage.vue,作为内容管理页面。
<template> <div class="manage-container"> <h2>日推内容管理</h2> <el-button type="primary" @click="handleCreate" :icon="Plus">新增推荐</el-button> <el-table :data="tableData" style="width: 100%; margin-top: 20px" border> <el-table-column prop="id" label="ID" width="80"></el-table-column> <el-table-column prop="title" label="标题"></el-table-column> <el-table-column prop="recommendDate" label="推荐日期" width="120"> <template #default="scope"> {{ formatDate(scope.row.recommendDate) }} </template> </el-table-column> <el-table-column prop="isPublished" label="发布状态" width="100"> <template #default="scope"> <el-tag :type="scope.row.isPublished ? 'success' : 'info'"> {{ scope.row.isPublished ? '已发布' : '未发布' }} </el-tag> </template> </el-table-column> <el-table-column label="操作" width="180"> <template #default="scope"> <el-button size="small" @click="handleEdit(scope.row)">编辑</el-button> <el-button size="small" type="danger" @click="handleDelete(scope.row.id)">删除</el-button> </template> </el-table-column> </el-table> <!-- 新增/编辑对话框 --> <el-dialog v-model="dialogVisible" :title="dialogTitle" width="50%"> <el-form :model="form" label-width="100px"> <el-form-item label="标题" required> <el-input v-model="form.title" placeholder="请输入推荐标题"></el-input> </el-form-item> <el-form-item label="推荐日期" required> <el-date-picker v-model="form.recommendDate" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" :disabled="isEditMode" /> </el-form-item> <el-form-item label="封面图URL"> <el-input v-model="form.imageUrl" placeholder="请输入图片链接"></el-input> </el-form-item> <el-form-item label="内容"> <el-input v-model="form.content" type="textarea" :rows="4" placeholder="请输入推荐内容"></el-input> </el-form-item> </el-form> <template #footer> <span class="dialog-footer"> <el-button @click="dialogVisible = false">取消</el-button> <el-button type="primary" @click="submitForm">确认</el-button> </span> </template> </el-dialog> </div> </template> <script setup> import { ref, onMounted, computed } from 'vue' import { Plus } from '@element-plus/icons-vue' import { ElMessage, ElMessageBox } from 'element-plus' import { getAllRecommendations, createRecommendation, updateRecommendation, deleteRecommendation } from '@/api/recommendation' const tableData = ref([]) const dialogVisible = ref(false) const form = ref({ id: null, title: '', recommendDate: '', imageUrl: '', content: '', isPublished: false }) const isEditMode = ref(false) const dialogTitle = computed(() => isEditMode.value ? '编辑推荐' : '新增推荐') const fetchData = async () => { try { const res = await getAllRecommendations() tableData.value = res } catch (error) { ElMessage.error('获取数据失败') } } const handleCreate = () => { isEditMode.value = false form.value = { id: null, title: '', recommendDate: '', imageUrl: '', content: '', isPublished: false } dialogVisible.value = true } const handleEdit = (row) => { isEditMode.value = true // 深拷贝,避免直接修改表格数据 form.value = { ...row } // 日期格式转换 form.value.recommendDate = row.recommendDate dialogVisible.value = true } const submitForm = async () => { // 简单校验 if (!form.value.title || !form.value.recommendDate) { ElMessage.warning('请填写标题和推荐日期') return } try { if (isEditMode.value) { await updateRecommendation(form.value.id, form.value) ElMessage.success('更新成功') } else { await createRecommendation(form.value) ElMessage.success('创建成功') } dialogVisible.value = false fetchData() // 刷新列表 } catch (error) { ElMessage.error('操作失败') } } const handleDelete = (id) => { ElMessageBox.confirm('确认删除这条推荐吗?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(async () => { try { await deleteRecommendation(id) ElMessage.success('删除成功') fetchData() } catch (error) { ElMessage.error('删除失败') } }).catch(() => {}) } const formatDate = (dateStr) => { if (!dateStr) return '' return new Date(dateStr).toLocaleDateString() } onMounted(() => { fetchData() }) </script>3.4 实现今日推荐展示页
创建src/views/TodayRecommendation.vue,作为用户查看今日推荐的页面。
<template> <div class="today-container" v-loading="loading"> <div v-if="recommendation" class="recommendation-card"> <h1>{{ recommendation.title }}</h1> <p class="date">推荐日期:{{ formatDate(recommendation.recommendDate) }}</p> <img v-if="recommendation.imageUrl" :src="recommendation.imageUrl" alt="封面" class="cover-image" /> <div class="content" v-html="formatContent(recommendation.content)"></div> </div> <div v-else class="empty-tip"> <el-empty description="今日暂无推荐内容" /> </div> </div> </template> <script setup> import { ref, onMounted } from 'vue' import { getTodayRecommendation } from '@/api/recommendation' import { ElMessage } from 'element-plus' const recommendation = ref(null) const loading = ref(false) const fetchToday = async () => { loading.value = true try { const res = await getTodayRecommendation() recommendation.value = res } catch (error) { // 404 错误是正常的,表示今天没有推荐 if (error.response && error.response.status === 404) { recommendation.value = null } else { ElMessage.error('获取今日推荐失败') } } finally { loading.value = false } } const formatDate = (dateStr) => { if (!dateStr) return '' return new Date(dateStr).toLocaleDateString('zh-CN', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }) } const formatContent = (content) => { if (!content) return '' // 简单处理,将换行转换为 <br>,生产环境应考虑安全过滤(如使用 DOMPurify) return content.replace(/\n/g, '<br>') } onMounted(() => { fetchToday() }) </script> <style scoped> .today-container { max-width: 800px; margin: 0 auto; padding: 20px; } .recommendation-card { background: #fff; padding: 30px; border-radius: 8px; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1); } .date { color: #888; font-size: 0.9em; margin-bottom: 20px; } .cover-image { max-width: 100%; border-radius: 4px; margin-bottom: 20px; } .content { line-height: 1.6; font-size: 1.1em; } .empty-tip { text-align: center; margin-top: 100px; } </style>3.5 配置路由并运行
在src/router/index.js中配置路由。
import { createRouter, createWebHistory } from 'vue-router' import TodayRecommendation from '@/views/TodayRecommendation.vue' import RecommendationManage from '@/views/RecommendationManage.vue' const routes = [ { path: '/', name: 'Today', component: TodayRecommendation }, { path: '/manage', name: 'Manage', component: RecommendationManage } ] const router = createRouter({ history: createWebHistory(), routes }) export default router修改App.vue,添加简单的导航。
<template> <div id="app"> <el-menu mode="horizontal" router> <el-menu-item index="/">今日推荐</el-menu-item> <el-menu-item index="/manage">内容管理</el-menu-item> </el-menu> <router-view /> </div> </template> <script setup> </script> <style> #app { font-family: Avenir, Helvetica, Arial, sans-serif; } </style>启动前端开发服务器:
npm run dev访问http://localhost:5173(Vite 默认端口) 即可看到应用。
4. 系统运行验证与联调
现在,我们启动前后端,验证整个流程是否跑通。
4.1 启动与基础功能验证
- 启动后端:在 IDE 中运行
DailyRecommendationApplication或在项目根目录执行mvn spring-boot:run。确保控制台无报错,并能在http://localhost:8081访问(虽然暂无页面)。 - 启动前端:在
umbrella-daily-recommendation-frontend目录执行npm run dev。访问http://localhost:5173。 - 验证前端路由:点击导航栏的“今日推荐”和“内容管理”,页面应能正常切换。
- 测试内容管理:
- 进入“内容管理”页,点击“新增推荐”。
- 填写标题、选择一个未来的日期作为推荐日期、填写内容。
- 点击“确认”,列表应刷新并显示新条目,状态为“未发布”。
- 可以继续新增多条,注意推荐日期不能重复。
- 测试今日推荐:
- 回到“今日推荐”页面。由于我们创建的内容推荐日期是未来的,所以此时应显示“今日暂无推荐内容”。
- 在“内容管理”页,找到一条推荐日期是“今天”的记录,点击“编辑”,将
isPublished手动改为true并保存(或者直接修改数据库)。刷新“今日推荐”页面,应能看到该条内容被展示出来。
4.2 验证定时任务
定时任务配置为每天凌晨1点执行,我们不可能等到那个时间测试。在开发阶段,有几种验证方式:
方式一:修改 Cron 表达式进行快速测试将DailyRecommendationService中的@Scheduled(cron = “0 0 1 * * ?”)临时改为更频繁的表达式,例如@Scheduled(cron = “*/30 * * * * ?”)表示每30秒执行一次。重启后端,观察日志,看任务是否按时执行,并正确找到了“明天”的推荐内容进行发布。
注意:测试完成后务必改回生产环境所需的表达式,或通过配置文件注入不同的表达式用于不同环境。
方式二:手动调用任务方法可以通过编写一个临时的测试接口或使用 Spring Boot Actuator 的端点来手动触发任务。这里展示一个简单的测试接口:
@RestController @RequestMapping("/api/test") @RequiredArgsConstructor public class TestController { private final DailyRecommendationService service; @PostMapping("/trigger-publish") public String triggerPublish() { service.publishScheduledRecommendation(); return "定时任务已手动触发"; } }通过 Postman 或 curl 调用POST /api/test/trigger-publish即可模拟任务执行。
方式三:查看 H2 数据库访问http://localhost:8081/h2-console,登录后执行SELECT * FROM DAILY_RECOMENDATION;,观察IS_PUBLISHED字段和RECOMEND_DATE字段的变化,确认任务逻辑正确。
4.3 核心流程验证清单
完成以下检查,确保核心流程无误:
| 步骤 | 操作 | 预期结果 | 验证方式 |
|---|---|---|---|
| 1. 数据创建 | 在管理页面创建一条推荐日期为“明天”的内容。 | 数据成功保存,is_published为false。 | 前端列表显示,H2 控制台查询确认。 |
| 2. 定时任务执行 | 等待定时任务触发(或手动触发)。 | 该条内容的is_published变为true。 | 查看后端日志,或 H2 控制台查询。 |
| 3. 前端获取今日推荐 | 当系统日期变为该条内容的推荐日期时,访问“今日推荐”页。 | 页面成功展示该条内容。 | 浏览器访问页面查看。 |
| 4. 日期唯一性约束 | 尝试创建两条推荐日期相同的记录。 | 第二条创建失败(后端应返回错误)。 | 前端弹出错误提示,H2 控制台确认只有一条。 |
| 5. 空状态处理 | 当没有已发布的今日推荐时。 | “今日推荐”页面显示友好的空状态提示。 | 浏览器访问页面查看。 |
5. 常见问题排查与优化实践
在开发和部署此系统时,你可能会遇到以下典型问题。
5.1 后端常见问题
问题1:定时任务不执行
- 现象:日志中没有定时任务的执行记录。
- 排查:
- 检查主应用类是否添加了
@EnableScheduling注解。 - 检查
@Scheduled注解的方法是否在 Spring 管理的 Bean 中(如@Service,@Component)。 - 检查 Cron 表达式是否正确。可以使用在线 Cron 表达式验证工具。
- 检查应用日志级别,确保能输出
DEBUG或INFO级别的日志。
- 检查主应用类是否添加了
- 解决:确保注解和表达式正确,并查看启动日志确认定时任务线程池已初始化。
问题2:H2 数据库数据重启后丢失
- 现象:每次重启 Spring Boot 应用,之前的数据都没了。
- 原因:配置中使用了
jdbc:h2:mem:dbname,这是纯内存模式。 - 解决(开发阶段):改为文件模式
jdbc:h2:file:./data/daily_recommend_db,数据会持久化到项目目录下的data文件夹中。生产环境应切换为 MySQL、PostgreSQL 等。
问题3:前端调用 API 跨域(CORS)错误
- 现象:浏览器控制台报错
Access-Control-Allow-Origin。 - 原因:前端运行在
localhost:5173,后端在localhost:8081,端口不同导致跨域。 - 解决:
- 后端解决(推荐):使用
@CrossOrigin注解(如我们代码所示)或配置全局 CORS 过滤器。 - 前端代理(开发阶段):在 Vite 配置 (
vite.config.js) 中设置代理,将/api请求转发到后端。
然后前端请求的export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:8081', changeOrigin: true, } } } })baseURL改为‘/api’。
- 后端解决(推荐):使用
5.2 前端常见问题
问题1:Element Plus 组件样式未加载
- 现象:组件功能正常,但样式丢失。
- 排查:检查
main.js中是否正确引入了‘element-plus/dist/index.css’。 - 解决:确保样式文件被导入。如果使用按需导入,需检查按需导入插件配置。
问题2:日期显示格式不正确
- 现象:后端返回的
LocalDate(如2023-10-27) 在前端显示为字符串或时间戳。 - 解决:使用
dayjs或date-fns等库进行格式化,或像示例中一样使用toLocaleDateString进行简单格式化。
问题3:编辑表单回显数据时,日期选择器不显示
- 现象:点击编辑,对话框打开了,但日期选择器是空的。
- 原因:Element Plus 的
el-date-picker的v-model绑定的日期字符串格式可能与组件期望的格式不匹配。 - 解决:确保
form.recommendDate的值是YYYY-MM-DD格式的字符串(通过value-format属性指定),或者是一个标准的 Date 对象。
5.3 生产环境部署建议
原型用于学习,上生产环境还需考虑以下几点:
- 数据库迁移:将 H2 替换为生产级数据库(如 MySQL/PostgreSQL)。更新
application.yml中的datasource配置,并考虑使用 Flyway 或 Liquibase 管理数据库版本。 - 配置外置:将 Cron 表达式、数据库连接信息、服务器端口等配置移到
application-prod.yml或环境变量中。 - 安全性:
- API 保护:为管理接口 (
/api/recommendations) 添加认证(如 JWT)和授权。 - 输入校验:在 Controller 或 Entity 中使用
@NotBlank,@Size等注解进行校验。 - 防止 XSS:前端展示
content时,如果使用v-html,务必对内容进行消毒(使用DOMPurify等库)。
- API 保护:为管理接口 (
- 定时任务高可用:在集群部署时,Spring 自带的
@Scheduled会在每个节点都执行,可能导致重复执行。需要引入分布式锁(如基于 Redis)或使用专门的分布式任务调度框架(如 Quartz Cluster, XXL-Job)。 - 日志与监控:配置完整的日志框架(如 Logback),将日志收集到 ELK 或类似系统中。添加健康检查端点 (
/actuator/health) 和指标端点 (/actuator/metrics)。 - 前端部署:使用
npm run build构建生产版本,将生成的dist目录内容部署到 Nginx 或对象存储(如 AWS S3, 阿里云 OSS)中。
通过以上步骤,你不仅完成了一个可运行的“Umbrella | 日推”系统原型,更掌握了从设计、开发、联调到排查的全流程。这个原型是骨架,你可以在此基础上增加更复杂的推荐算法、用户偏好分析、多端推送(如邮件、消息)等功能,将其演进为一个真正实用的每日推荐系统。