--- name: 打卡视频生成与展示 overview: 实现打卡勾选生成视频后,在饮食记录页面定时查询视频任务状态并显示,在打卡详情支持视频播放,勾选生成视频的打卡帖子在社区最新tab显示 todos: - id: backend-checkin-article content: 修改打卡提交逻辑,勾选视频时创建eb_article记录存储taskId status: completed - id: backend-list-api content: 更新打卡列表/详情接口,关联article表返回videoUrl和videoStatus status: completed - id: backend-callback content: 完善KieAI回调处理,更新article的video_url和status_task status: completed - id: backend-community content: 更新社区列表接口,关联打卡记录的视频信息 status: completed - id: frontend-dietary-records content: 饮食记录页面增加视频状态显示和定时轮询逻辑 status: completed - id: frontend-detail-video content: 打卡详情页增加视频播放器组件 status: completed - id: frontend-community-badge content: 社区页面卡片增加视频标记 status: completed - id: frontend-api content: 新增getVideoTaskStatus API方法 status: completed isProject: false --- # 打卡视频生成与展示功能实现计划 ## 背景分析 根据代码探索,当前系统已具备: - **打卡发布**: `[checkin-publish.vue](msh_single_uniapp/pages/tool/checkin-publish.vue)` 支持勾选"生成打卡视频分享到社区",会调用KieAI创建视频任务并返回 `taskId` - **数据存储**: `eb_user_sign` 表存储 `task_id` 和 `enable_ai_video` 字段,`eb_article` 表存储视频URL和任务状态 - **视频生成**: 通过 `[ToolSora2Service](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolSora2ServiceImpl.java)` 调用KieAI API - **状态查询接口**: `GET /api/front/kieai/video/task/{taskId}` 已存在 **缺失功能**: 1. 打卡时未创建 `eb_article` 记录来存储视频 2. 饮食记录页面不显示视频任务状态 3. 打卡详情页不支持视频播放 4. 社区帖子未关联视频信息 ## 实现方案 ### 数据库层面 #### 1. 修改表结构 **eb_user_sign表**: 已有 `task_id`, `enable_ai_video` 字段,无需修改 **v2_community_posts表**: 已有 `video_url` 字段(从SQL看到),需确认Java实体类是否包含 **eb_article表**: 已有 `video_url`, `task_id`, `status_task`, `check_in_record_id` 字段 #### 2. 数据流设计 ```mermaid flowchart TD A[用户勾选生成视频并发布打卡] --> B[前端:调用KieAI创建视频任务] B --> C[前端:获得taskId] C --> D[前端:提交打卡 带taskId+enableAIVideo] D --> E[后端:保存eb_user_sign记录] E --> F[后端:创建eb_article记录] F --> G[eb_article: taskId, statusTask=0, checkInRecordId] H[饮食记录页面] --> I[加载打卡列表API] I --> J[返回 taskId, enableAIVideo, videoUrl] J --> K[前端:检测有taskId且无videoUrl] K --> L[启动定时轮询 /kieai/video/task/taskId] L --> M{任务状态?} M -->|进行中| N[显示生成中状态] M -->|完成| O[更新videoUrl到article] M -->|失败| P[显示失败状态] O --> Q[页面刷新后显示视频] R[打卡详情页] --> S[查询详情API] S --> T[返回taskId+videoUrl] T --> U{有videoUrl?} U -->|是| V[显示video标签播放] U -->|否+有taskId| W[显示生成中状态] X[社区最新tab] --> Y[查询v2_community_posts] Y --> Z[关联eb_user_sign.enableAIVideo] Z --> AA[显示有视频标记的帖子] ``` ### 后端改动 #### 1. 更新打卡提交逻辑 `[ToolCheckinServiceImpl.submit()](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolCheckinServiceImpl.java)` - 在打卡成功后,如果 `enableAIVideo=1` 且有 `taskId`,创建 `eb_article` 记录: - `type = 2` (视频类型) - `task_id = taskId` - `status_task = 0` (已创建) - `check_in_record_id = userSign.id` - `title` = 打卡备注或默认标题 - 其他字段按现有逻辑填充 #### 2. 更新打卡列表接口 `[ToolCheckinServiceImpl.getList()](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolCheckinServiceImpl.java)` - 查询时关联 `eb_article` 表(通过 `eb_user_sign.task_id = eb_article.task_id`) - 返回字段增加: - `videoUrl`: 从 `eb_article.video_url` 获取 - `videoStatus`: 从 `eb_article.status_task` 获取 (0=生成中, 1=完成, 2=失败) - `taskId`: 已有 - `enableAIVideo`: 已有 #### 3. 更新打卡详情接口 `[ToolCheckinServiceImpl.getDetail()](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolCheckinServiceImpl.java)` - 增加返回 `videoUrl` 和 `videoStatus` 字段 #### 4. 完善KieAI回调处理 `[KieAIController.handleCallback()](msh_crmeb_22/crmeb-front/src/main/java/com/zbkj/front/controller/KieAIController.java)` - 当前只记录日志,需改为: - 解析回调参数获取 `taskId`, `state`, `result_urls` - 根据 `taskId` 更新 `eb_article` 表: - `status_task = 1` (成功) 或 `2` (失败) - `video_url = result_urls[0]` 参考 `models-integration` 项目的实现: ```java articleMapper.updateVideoUrlAndTaskStatusByTaskId(taskId, videoUrl, statusTask); ``` #### 5. 更新社区列表接口 `[ToolCommunityServiceImpl.getList()](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolCommunityServiceImpl.java)` - 当前已关联 `eb_user_sign` 获取 `mealType` - 同样方式关联获取 `enable_ai_video` 和 `task_id` - 通过 `task_id` 关联 `eb_article` 获取 `video_url` - 返回字段增加: - `hasVideo`: boolean (是否有视频) - `videoUrl`: 视频地址 - `enableAIVideo`: 是否启用了视频生成 #### 6. 更新V2CommunityPost实体类 `[V2CommunityPost.java](msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/model/tool/V2CommunityPost.java)` - 确认是否已有 `videoUrl` 字段,若无则添加: ```java @ApiModelProperty(value = "视频地址") private String videoUrl; ``` ### 前端改动 #### 1. 饮食记录页面增加视频状态显示 `[dietary-records.vue](msh_single_uniapp/pages/tool/dietary-records.vue)` **显示逻辑**: - 列表项增加视频状态角标: - 有 `videoUrl`: 显示"📹视频"标记 - 有 `taskId` 但无 `videoUrl`: 显示"⏳生成中"或进度条 - `videoStatus = 2`: 显示"❌生成失败" **轮询逻辑**: ```javascript data() { return { pollingTasks: [], // 需要轮询的任务列表 pollingTimer: null } }, methods: { // 启动轮询 startPolling() { if (this.pollingTimer) return; this.pollingTimer = setInterval(() => { this.pollVideoTasks(); }, 5000); // 每5秒查询一次 }, // 轮询视频任务状态 async pollVideoTasks() { const tasksToCheck = this.recordList.filter(item => item.enableAIVideo && item.taskId && !item.videoUrl ); if (tasksToCheck.length === 0) { this.stopPolling(); return; } for (const task of tasksToCheck) { try { const res = await getVideoTaskStatus(task.taskId); if (res.code === 200 && res.data) { const status = res.data.state; if (status === 'success') { // 刷新列表以获取最新视频URL this.loadRecordList(); break; } else if (status === 'failed') { // 标记失败 task.videoStatus = 2; } } } catch (error) { console.error('查询视频任务失败:', error); } } }, // 停止轮询 stopPolling() { if (this.pollingTimer) { clearInterval(this.pollingTimer); this.pollingTimer = null; } } } ``` **生命周期**: ```javascript onLoad() { this.loadRecordList(); this.startPolling(); }, onUnload() { this.stopPolling(); }, onShow() { // 从其他页面返回时重新检查 this.loadRecordList(); this.startPolling(); } ``` **模板增加视频状态标记**: ```vue 📹 有视频 ❌ 生成失败 ⏳ 生成中 ``` #### 2. 打卡详情页增加视频播放 `[checkin-detail.vue](msh_single_uniapp/pages/tool/checkin-detail.vue)` **数据结构更新**: ```javascript data() { return { checkinData: { // ...现有字段 videoUrl: '', taskId: '', enableAIVideo: false, videoStatus: 0 } } } ``` **模板增加视频播放器**: ```vue 🎬 打卡视频 视频生成中,请稍后... ``` **formatCheckinData更新**: ```javascript formatCheckinData(item) { // ...现有逻辑 this.checkinData = { // ...现有字段 videoUrl: item.videoUrl || '', taskId: item.taskId || '', enableAIVideo: item.enableAIVideo || false, videoStatus: item.videoStatus || 0 }; } ``` #### 3. 社区页面显示视频标记 `[community.vue](msh_single_uniapp/pages/tool_main/community.vue)` **formatPostList更新**: ```javascript formatPostList(list) { return list.map(item => { // ...现有逻辑 return { // ...现有字段 hasVideo: item.hasVideo || false, videoUrl: item.videoUrl || '', enableAIVideo: item.enableAIVideo || false }; }); } ``` **卡片增加视频标记**: ```vue 🎬 视频 ``` **样式**: ```scss .video-badge { position: absolute; top: 8px; right: 8px; background: rgba(0, 0, 0, 0.6); border-radius: 12px; padding: 4px 10px; display: flex; align-items: center; gap: 4px; .badge-icon { font-size: 14px; } .badge-text { font-size: 12px; color: #fff; } } ``` #### 4. 新增API方法 `[tool.js](msh_single_uniapp/api/tool.js)` ```javascript /** * 查询视频任务状态 * @param {String} taskId - 任务ID */ export function getVideoTaskStatus(taskId) { return request.get(`kieai/video/task/${taskId}`, { apiKey: 'your-api-key' // 从配置获取 }); } ``` ### 关键技术细节 #### 1. 轮询策略 - **触发条件**: 列表中存在 `enableAIVideo=true` 且 `taskId` 不为空但 `videoUrl` 为空的记录 - **轮询间隔**: 5秒 - **停止条件**: - 所有任务都有 `videoUrl` 或 `videoStatus=2` - 页面销毁(onUnload) - 超过最大轮询次数(如60次,即5分钟) - **优化**: 只轮询当前可见列表中的任务,避免全量查询 #### 2. 视频播放兼容性 - 使用uni-app的 `