20 KiB
广场作品TaskId查询功能说明
📋 功能概述
新增了通过任务编号(taskId/taskNo)查询作品详情的功能,具有以下特点:
- ✅ 使用taskId查询时不会增加浏览数,避免对作品热度数据产生影响
- ✅ 支持查询未发布的任务,即使任务还没发布到广场也能查看详情
- ✅ 自动大小写转换,兼容大小写输入
🎯 业务需求
在某些场景下(如用户从自己的任务列表查看作品),需要通过任务编号直接查看详情,包括:
- 已发布作品:查看广场作品的完整信息(包含浏览数、点赞数等社交数据)
- 未发布任务:查看任务的基本信息(不包含社交数据)
此时不应计入公开浏览数,以保证统计数据的准确性。
🔧 实现方案
1. 接口修改
接口路径: GET /user/plaza/works/{identifier}
新增查询参数: queryType
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
queryType |
String | 否 | workNo |
查询类型: - workNo: 通过作品编号查询(记录浏览)- taskId: 通过任务编号查询(不记录浏览) |
📡 接口使用示例
示例1:通过作品编号查询(默认方式,记录浏览)
curl -X 'GET' \
'http://116.62.4.26:8081/user/plaza/works/WORK-1762440663366-3093' \
-H 'accept: */*'
或明确指定:
curl -X 'GET' \
'http://116.62.4.26:8081/user/plaza/works/WORK-1762440663366-3093?queryType=workNo' \
-H 'accept: */*'
行为:
- ✅ 返回作品详情
- ✅ 增加浏览数 +1
示例2:通过任务编号查询(不记录浏览)
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 方法
@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,跳过recordViewworkNo模式:保持原有逻辑,先记录浏览再查询
2. 服务接口层
文件: PlazaService.java
新增方法:
/**
* 通过任务编号查询作品详情(不记录浏览)
*
* @param userId 当前用户ID(可为null)
* @param taskNo 任务编号
* @return 作品详情
*/
WorkDetailResponse getWorkDetailByTaskNo(Long userId, String taskNo);
3. 服务实现层
文件: PlazaServiceImpl.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查询:
/**
* 通过任务编号查询作品详情(包含作者信息)
*/
@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.util.Map<String, Object> findTaskDetailByTaskNo(@Param("taskNo") String taskNo);
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:查询已发布到广场的作品
{
"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 任务已完成
{
"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 任务处理中
{
"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 任务失败
{
"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?
-
任务未完成:
taskStatus为queued或processing- 解决方案:轮询查询,等待任务完成
-
任务失败:
taskStatus为failed- 解决方案:显示
errorMessage给用户
- 解决方案:显示
-
任务刚完成:数据库更新延迟
- 解决方案:刷新重试
为什么imageUrl为null?
- text_to_video任务:文本生成视频,不需要参考图 ✅ 正常
- 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. 异常处理
// taskId不存在时的错误提示
throw new IllegalArgumentException("该任务对应的作品不存在: " + taskNo);
// workNo不存在时的错误提示
throw new IllegalArgumentException("作品不存在: " + workNo);
📈 数据库优化建议
推荐索引
-- 任务编号索引(如果尚未创建)
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查询(记录浏览)
# 第一次查询
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查询(不记录浏览)
# 查询前记录当前浏览数
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
curl 'http://116.62.4.26:8081/user/plaza/works/TASK-invalid?queryType=taskId'
# 预期响应:
{
"code": 400,
"message": "该任务对应的作品不存在: TASK-invalid",
"data": null
}
测试用例4: 未发布作品的taskId
# 对于未发布到广场的任务
curl 'http://116.62.4.26:8081/user/plaza/works/TASK-未发布?queryType=taskId'
# 预期响应:
{
"code": 400,
"message": "该任务对应的作品不存在: TASK-未发布",
"data": null
}
📝 日志记录
通过workNo查询的日志
INFO - 通过workNo查询作品详情,workNo: WORK-1762440663366-3093
DEBUG - 记录作品浏览,workNo: WORK-1762440663366-3093, userId: 17563793187762127
INFO - 查询作品详情,workNo: WORK-1762440663366-3093
通过taskId查询的日志
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查询作品详情该任务对应的作品不存在