Files
1818web-hoduan/广场作品TaskId查询功能说明.md
2025-11-14 17:41:15 +08:00

723 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 广场作品TaskId查询功能说明
## 📋 功能概述
新增了通过任务编号taskId/taskNo查询作品详情的功能具有以下特点
- ✅ 使用taskId查询时**不会增加浏览数**,避免对作品热度数据产生影响
-**支持查询未发布的任务**,即使任务还没发布到广场也能查看详情
- ✅ 自动大小写转换,兼容大小写输入
---
## 🎯 业务需求
在某些场景下(如用户从自己的任务列表查看作品),需要通过任务编号直接查看详情,包括:
1. **已发布作品**:查看广场作品的完整信息(包含浏览数、点赞数等社交数据)
2. **未发布任务**:查看任务的基本信息(不包含社交数据)
此时不应计入公开浏览数,以保证统计数据的准确性。
---
## 🔧 实现方案
### 1. 接口修改
**接口路径**: `GET /user/plaza/works/{identifier}`
**新增查询参数**: `queryType`
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|-------|------|------|--------|------|
| `queryType` | String | 否 | `workNo` | 查询类型:<br>- `workNo`: 通过作品编号查询(记录浏览)<br>- `taskId`: 通过任务编号查询(不记录浏览) |
---
## 📡 接口使用示例
### 示例1通过作品编号查询默认方式记录浏览
```bash
curl -X 'GET' \
'http://116.62.4.26:8081/user/plaza/works/WORK-1762440663366-3093' \
-H 'accept: */*'
```
或明确指定:
```bash
curl -X 'GET' \
'http://116.62.4.26:8081/user/plaza/works/WORK-1762440663366-3093?queryType=workNo' \
-H 'accept: */*'
```
**行为**:
- ✅ 返回作品详情
- ✅ 增加浏览数 +1
---
### 示例2通过任务编号查询不记录浏览
```bash
curl -X 'GET' \
'http://116.62.4.26:8081/user/plaza/works/TASK-20251026183750127-8554?queryType=taskId' \
-H 'accept: */*'
```
**行为**:
- ✅ 返回作品详情(已发布)或任务详情(未发布)
-**不增加浏览数**(保持原有数据)
**返回数据区别**:
- 如果已发布到广场:返回完整作品信息,包含标题、描述、标签、浏览数、点赞数等
- 如果未发布返回任务基本信息社交数据浏览数、点赞数等为0标题/描述/标签为空
---
## 🔄 业务流程对比
### 通过workNo查询原有逻辑
```
用户请求 GET /plaza/works/{workNo}
提取当前用户ID
【记录浏览】增加view_count +1
查询作品详情(联表查询)
检查点赞状态
返回作品详情
```
### 通过taskId查询新增逻辑
```
用户请求 GET /plaza/works/{taskNo}?queryType=taskId
提取当前用户ID
【跳过浏览记录】不增加view_count
【步骤1】查询广场作品表plaza_work
├── 找到 → 返回完整作品详情(含社交数据)
└── 未找到 → 继续下一步
【步骤2】查询AI任务表ai_task
├── 找到 → 返回任务基本信息社交数据为0
└── 未找到 → 抛出"任务不存在"异常
返回详情
```
---
## 💻 代码实现
### 1. 控制器层修改
**文件**: `PlazaController.java`
**修改位置**: `getWorkDetail` 方法
```java
@GetMapping("/works/{workNo}")
public Result<WorkDetailResponse> getWorkDetail(
@PathVariable String workNo,
@RequestParam(defaultValue = "workNo") String queryType) {
try {
Long userId = SecurityUtil.getCurrentUserIdOrNull();
WorkDetailResponse response;
// 根据查询类型处理
if ("taskId".equalsIgnoreCase(queryType)) {
// 通过taskId查询不记录浏览
log.info("通过taskId查询作品详情taskNo: {}", workNo);
response = plazaService.getWorkDetailByTaskNo(userId, workNo);
} else {
// 通过workNo查询记录浏览
log.info("通过workNo查询作品详情workNo: {}", workNo);
plazaService.recordView(workNo, userId);
response = plazaService.getWorkDetail(userId, workNo);
}
return Result.success(response);
} catch (Exception e) {
log.error("查询作品详情失败identifier: {}, queryType: {}", workNo, queryType, e);
return Result.error(e.getMessage());
}
}
```
**关键逻辑**:
- 通过 `queryType` 参数判断查询类型
- `taskId` 模式:直接调用 `getWorkDetailByTaskNo`,跳过 `recordView`
- `workNo` 模式:保持原有逻辑,先记录浏览再查询
---
### 2. 服务接口层
**文件**: `PlazaService.java`
**新增方法**:
```java
/**
* 通过任务编号查询作品详情(不记录浏览)
*
* @param userId 当前用户ID可为null
* @param taskNo 任务编号
* @return 作品详情
*/
WorkDetailResponse getWorkDetailByTaskNo(Long userId, String taskNo);
```
---
### 3. 服务实现层
**文件**: `PlazaServiceImpl.java`
**新增实现**:
```java
@Override
public WorkDetailResponse getWorkDetailByTaskNo(Long userId, String taskNo) {
log.info("通过任务编号查询作品详情不记录浏览taskNo: {}", taskNo);
// 1. 统一转换为大写(兼容大小写输入)
String normalizedTaskNo = taskNo.toUpperCase();
// 2. 先查询广场作品(已发布)
Map<String, Object> workMap = plazaWorkMapper.findWorkDetailByTaskNo(normalizedTaskNo);
if (workMap != null) {
// 2.1 找到广场作品,检查是否点赞
boolean isLiked = false;
if (userId != null) {
Long workId = (Long) workMap.get("id");
isLiked = plazaWorkLikeMapper.existsByWorkIdAndUserId(workId, userId) > 0;
}
return convertToDetailResponse(workMap, isLiked);
}
// 3. 广场中没找到查询AI任务表未发布的任务
log.info("广场中未找到作品,尝试从任务表查询 - taskNo: {}", normalizedTaskNo);
Map<String, Object> taskMap = aiTaskMapper.findTaskDetailByTaskNo(normalizedTaskNo);
if (taskMap == null) {
throw new IllegalArgumentException("该任务不存在: " + taskNo);
}
// 4. 从任务数据构建作品详情响应(未发布状态)
return convertTaskToWorkDetail(taskMap);
}
```
**特点**:
-**两级查询**:优先查询广场作品,未找到则查询任务表
-**大小写兼容**:自动转换为大写,兼容小写输入
-**完整回退**:未发布的任务也能正常返回
-**不记录浏览**:保证浏览数不变
---
### 4. 数据访问层
#### PlazaWorkMapper查询广场作品
**文件**: `PlazaWorkMapper.java`
**新增SQL查询**:
```java
/**
* 通过任务编号查询作品详情(包含作者信息)
*/
@Select("SELECT pw.id, pw.work_no, pw.user_id, pw.task_no, pw.task_type, pw.model_name, " +
" pw.prompt, pw.result_url, pw.image_url, pw.aspect_ratio, pw.title, " +
" pw.description, pw.tags, pw.view_count, pw.like_count, pw.share_count, " +
" pw.comment_count, pw.is_public, pw.status, pw.create_time, pw.update_time, " +
" u.username as nickname, u.avatar_url " +
"FROM plaza_work pw " +
"LEFT JOIN user u ON pw.user_id = u.id " +
"WHERE pw.task_no = #{taskNo} AND pw.is_deleted = 0")
Map<String, Object> findWorkDetailByTaskNo(@Param("taskNo") String taskNo);
```
#### AiTaskMapper查询AI任务
**文件**: `AiTaskMapper.java``AiTaskMapper.xml`
**新增方法**:
```java
/**
* 通过任务编号查询任务详情(包含作者信息)
*/
java.util.Map<String, Object> findTaskDetailByTaskNo(@Param("taskNo") String taskNo);
```
**XML实现**:
```xml
<!-- 通过任务编号查询任务详情(包含作者信息) -->
<select id="findTaskDetailByTaskNo" resultType="java.util.HashMap">
SELECT
at.id,
at.task_no,
at.user_id,
at.model_name,
at.task_type,
at.prompt,
at.result_url,
at.image_url,
at.aspect_ratio,
at.status,
at.progress,
at.error_message,
at.create_time,
at.update_time,
u.username as nickname,
u.avatar_url
FROM ai_task at
LEFT JOIN user u ON at.user_id = u.id
WHERE at.task_no = #{taskNo} AND at.is_deleted = 0
</select>
```
**SQL特点**:
- 查询AI任务表的基本字段
- 通过LEFT JOIN关联用户信息
- 包含任务状态、进度、错误信息等
---
## 📊 响应数据结构
### 情况1查询已发布到广场的作品
```json
{
"code": 200,
"message": "success",
"data": {
"workNo": "WORK-1762440663366-3093",
"taskNo": "TASK-20251026183750127-8554",
"taskType": "text_to_video",
"modelName": "minimax-video-01",
"prompt": "战争场景,恢弘气势...",
"resultUrl": "https://oss.example.com/videos/xxx.mp4",
"imageUrl": "https://oss.example.com/images/ref.jpg",
"aspectRatio": "16:9",
"title": "战场气氛短视频",
"description": "根据参考图生成的恢弘战场场景",
"tags": ["视频", "战争", "特效"],
"viewCount": 125,
"likeCount": 38,
"shareCount": 5,
"isLiked": true,
"author": {
"userId": 17563793187762127,
"nickname": "用户123456",
"avatarUrl": "https://img.remit.ee/api/file/xxx.png"
},
"createTime": "2024-11-06T10:30:00",
"updateTime": "2024-11-07T15:45:00"
}
}
```
### 情况2查询未发布的任务
#### 2.1 任务已完成
```json
{
"code": 200,
"message": "success",
"data": {
"workNo": null, // 未发布,无作品编号
"taskNo": "TASK-20251106195754218-9940",
"taskType": "text_to_video",
"modelName": "minimax-video-01",
"prompt": "战争场景,恢弘气势...",
"resultUrl": "https://oss.example.com/videos/xxx.mp4", // ✅ 已完成,有结果
"imageUrl": null, // text_to_video 任务通常无参考图
"aspectRatio": "16:9",
"title": null, // 未发布,无标题
"description": null, // 未发布,无描述
"tags": [], // 未发布,无标签
"viewCount": 0, // 未发布浏览数为0
"likeCount": 0, // 未发布点赞数为0
"shareCount": 0, // 未发布分享数为0
"isLiked": false, // 未发布,不可点赞
"author": {
"userId": 17563793187762127,
"nickname": "用户123456",
"avatarUrl": "https://img.remit.ee/api/file/xxx.png"
},
"createTime": "2024-11-06T19:57:54",
"updateTime": "2024-11-06T20:05:30",
"taskStatus": "completed", // ✅ 任务状态:已完成
"taskProgress": 100, // ✅ 任务进度100%
"errorMessage": null // ✅ 无错误
}
}
```
#### 2.2 任务处理中
```json
{
"code": 200,
"message": "success",
"data": {
"workNo": null,
"taskNo": "TASK-20251106195754218-9940",
"taskType": "text_to_video",
"modelName": "minimax-video-01",
"prompt": "战争场景,恢弘气势...",
"resultUrl": null, // ⏳ 处理中,暂无结果
"imageUrl": null,
"aspectRatio": "16:9",
"title": null,
"description": null,
"tags": [],
"viewCount": 0,
"likeCount": 0,
"shareCount": 0,
"isLiked": false,
"author": {
"userId": 17563793187762127,
"nickname": "用户123456",
"avatarUrl": "https://img.remit.ee/api/file/xxx.png"
},
"createTime": "2024-11-06T19:57:54",
"updateTime": "2024-11-06T19:58:55",
"taskStatus": "processing", // ⏳ 任务状态:处理中
"taskProgress": 45, // ⏳ 任务进度45%
"errorMessage": null
}
}
```
#### 2.3 任务失败
```json
{
"code": 200,
"message": "success",
"data": {
"workNo": null,
"taskNo": "TASK-20251106195754218-9940",
"taskType": "text_to_video",
"modelName": "minimax-video-01",
"prompt": "战争场景,恢弘气势...",
"resultUrl": null, // ❌ 失败,无结果
"imageUrl": null,
"aspectRatio": "16:9",
"title": null,
"description": null,
"tags": [],
"viewCount": 0,
"likeCount": 0,
"shareCount": 0,
"isLiked": false,
"author": {
"userId": 17563793187762127,
"nickname": "用户123456",
"avatarUrl": "https://img.remit.ee/api/file/xxx.png"
},
"createTime": "2024-11-06T19:57:54",
"updateTime": "2024-11-06T20:10:30",
"taskStatus": "failed", // ❌ 任务状态:失败
"taskProgress": 0, // ❌ 任务进度0%
"errorMessage": "生成超时或服务异常" // ❌ 错误信息
}
}
```
**关键区别**:
| 字段 | 已发布作品 | 未发布任务 |
|-----|-----------|-----------|
| `workNo` | 有值 | `null` |
| `title` | 用户设置的标题 | `null` |
| `description` | 用户设置的描述 | `null` |
| `tags` | 用户设置的标签数组 | `[]` 空数组 |
| `viewCount` | 真实浏览数 | `0` |
| `likeCount` | 真实点赞数 | `0` |
| `shareCount` | 真实分享数 | `0` |
| `isLiked` | 真实点赞状态 | `false` |
| `taskStatus` | `null` | 任务状态queued/processing/completed/failed |
| `taskProgress` | `null` | 任务进度0-100 |
| `errorMessage` | `null` | 错误信息(失败时才有) |
| `resultUrl` | 总是有值 | 仅completed时有值 |
| `imageUrl` | 可能有值 | text_to_video通常为null |
---
## 🔍 任务状态说明
### 任务状态taskStatus
| 状态 | 说明 | resultUrl | 前端建议 |
|-----|------|-----------|---------|
| `queued` | 排队中 | `null` | 显示"排队中..." |
| `processing` | 处理中 | `null` | 显示进度条使用taskProgress |
| `completed` | 已完成 | ✅ 有值 | 显示视频播放器 |
| `failed` | 失败 | `null` | 显示错误提示使用errorMessage |
### 为什么resultUrl为null
1. **任务未完成**`taskStatus``queued``processing`
- 解决方案:轮询查询,等待任务完成
2. **任务失败**`taskStatus``failed`
- 解决方案:显示 `errorMessage` 给用户
3. **任务刚完成**:数据库更新延迟
- 解决方案:刷新重试
### 为什么imageUrl为null
1. **text_to_video任务**:文本生成视频,不需要参考图 ✅ 正常
2. **image_to_video任务**图生视频应该有参考图URL
---
## 🎯 使用场景
### 场景1公开广场浏览使用workNo
**场景描述**: 用户在广场浏览其他人的作品
**调用方式**: `GET /plaza/works/{workNo}``?queryType=workNo`
**预期行为**:
- ✅ 记录浏览数
- ✅ 用于热度排序
- ✅ 统计作品受欢迎程度
---
### 场景2用户查看自己的作品使用taskId
**场景描述**: 用户从"我的任务"列表点击查看已发布的作品
**调用方式**: `GET /plaza/works/{taskNo}?queryType=taskId`
**预期行为**:
- ❌ 不记录浏览数(避免刷数据)
- ✅ 查看真实的浏览统计
- ✅ 预览作品效果
---
### 场景3外部分享链接使用workNo
**场景描述**: 通过分享链接访问作品
**调用方式**: `GET /plaza/works/{workNo}`
**预期行为**:
- ✅ 记录浏览数
- ✅ 统计分享传播效果
---
## 🔒 安全性考虑
### 1. 权限控制
- ✅ 两种查询方式都支持未登录用户访问
- ✅ 已登录用户可查看个性化点赞状态
- ✅ 查询条件都包含 `is_deleted = 0`,过滤已删除作品
### 2. 数据一致性
-`task_no``work_no` 在数据库中都有索引(建议)
- ✅ 一个任务最多对应一个作品(发布时已验证)
- ✅ 查询结果经过软删除过滤
### 3. 异常处理
```java
// taskId不存在时的错误提示
throw new IllegalArgumentException("该任务对应的作品不存在: " + taskNo);
// workNo不存在时的错误提示
throw new IllegalArgumentException("作品不存在: " + workNo);
```
---
## 📈 数据库优化建议
### 推荐索引
```sql
-- 任务编号索引(如果尚未创建)
CREATE INDEX idx_task_no ON plaza_work(task_no);
-- 作品编号索引(应该已存在)
CREATE UNIQUE INDEX uk_work_no ON plaza_work(work_no);
-- 复合索引(优化查询性能)
CREATE INDEX idx_task_no_deleted ON plaza_work(task_no, is_deleted);
```
---
## 🧪 测试建议
### 测试用例1: 通过workNo查询记录浏览
```bash
# 第一次查询
curl 'http://116.62.4.26:8081/user/plaza/works/WORK-xxx'
# 预期: viewCount = N
# 第二次查询
curl 'http://116.62.4.26:8081/user/plaza/works/WORK-xxx'
# 预期: viewCount = N + 1
```
---
### 测试用例2: 通过taskId查询不记录浏览
```bash
# 查询前记录当前浏览数
curl 'http://116.62.4.26:8081/user/plaza/works/WORK-xxx'
# 假设 viewCount = 100
# 通过taskId查询多次
curl 'http://116.62.4.26:8081/user/plaza/works/TASK-xxx?queryType=taskId'
curl 'http://116.62.4.26:8081/user/plaza/works/TASK-xxx?queryType=taskId'
curl 'http://116.62.4.26:8081/user/plaza/works/TASK-xxx?queryType=taskId'
# 再次通过workNo查询确认
curl 'http://116.62.4.26:8081/user/plaza/works/WORK-xxx'
# 预期: viewCount 仍然为 100未增加
```
---
### 测试用例3: 错误的taskId
```bash
curl 'http://116.62.4.26:8081/user/plaza/works/TASK-invalid?queryType=taskId'
# 预期响应:
{
"code": 400,
"message": "该任务对应的作品不存在: TASK-invalid",
"data": null
}
```
---
### 测试用例4: 未发布作品的taskId
```bash
# 对于未发布到广场的任务
curl 'http://116.62.4.26:8081/user/plaza/works/TASK-未发布?queryType=taskId'
# 预期响应:
{
"code": 400,
"message": "该任务对应的作品不存在: TASK-未发布",
"data": null
}
```
---
## 📝 日志记录
### 通过workNo查询的日志
```log
INFO - 通过workNo查询作品详情workNo: WORK-1762440663366-3093
DEBUG - 记录作品浏览workNo: WORK-1762440663366-3093, userId: 17563793187762127
INFO - 查询作品详情workNo: WORK-1762440663366-3093
```
### 通过taskId查询的日志
```log
INFO - 通过taskId查询作品详情taskNo: TASK-20251026183750127-8554
INFO - 通过任务编号查询作品详情不记录浏览taskNo: TASK-20251026183750127-8554
```
---
## ✅ 修改文件清单
| 文件路径 | 修改内容 |
|---------|---------|
| `PlazaController.java` | 新增 `queryType` 参数,根据类型分发请求 |
| `PlazaService.java` | 新增 `getWorkDetailByTaskNo()` 接口方法 |
| `PlazaServiceImpl.java` | 实现两级查询逻辑、大小写转换、`convertTaskToWorkDetail()` 转换方法 |
| `PlazaWorkMapper.java` | 新增 `findWorkDetailByTaskNo()` SQL查询 |
| `AiTaskMapper.java` | 新增 `findTaskDetailByTaskNo()` 接口方法 |
| `AiTaskMapper.xml` | 新增 `findTaskDetailByTaskNo` SQL实现关联user表 |
---
## 🎬 总结
**向后兼容** - 原有接口调用方式不受影响
**灵活查询** - 支持通过作品编号或任务编号查询
**完整回退** - 未发布任务也能正常查询
**大小写兼容** - 自动转换大小写,提升用户体验
**数据准确** - taskId查询不影响浏览统计
**完善日志** - 详细记录查询类型和来源
**异常处理** - 明确的错误提示信息
该功能完美满足了在不同场景下查询作品详情的需求,包括已发布作品和未发布任务,同时保证了统计数据的准确性!
---
## 🔍 Bug修复记录
### Bug 1: 大小写敏感问题
**现象**: 传入小写的taskId`task-xxx`)查询失败
**原因**: 数据库中存储的是大写格式(如 `TASK-xxx`
**解决**: 在所有查询方法中添加 `.toUpperCase()` 转换
**影响范围**: `getWorkDetail`, `getWorkDetailByTaskNo`, `likeWork`, `unlikeWork`, `deleteWork`, `recordView`
### Bug 2: 未发布任务无法查询
**现象**: 传入未发布任务的taskId返回"作品不存在"
**原因**: 只查询了plaza_work表未发布的任务不在该表中
**解决**: 实现两级查询先查plaza_work未找到再查ai_task表
**新增方法**: `AiTaskMapper.findTaskDetailByTaskNo()`, `PlazaServiceImpl.convertTaskToWorkDetail()`
---
## 📞 技术支持
如有问题,请查看日志文件或联系技术团队。
**日志关键字**:
- `通过taskId查询作品详情`
- `通过workNo查询作品详情`
- `该任务对应的作品不存在`