---
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的 `