Files
1818web-hoduan/广场作品TaskId查询功能说明.md

723 lines
20 KiB
Markdown
Raw Normal View History

# 广场作品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查询作品详情`
- `该任务对应的作品不存在`